欢迎来到Cefler的博客😁
🕌博客主页:折纸花满衣
🏠个人专栏:C++
前言
C++11标准引入了线程库,通过其可以在C++程序中方便地创建和管理线程。以下是一些常用的线程库组件:
-
std::thread :
std::thread
类用于表示一个线程,通过其构造函数可以传入一个可调用对象(函数或者lambda表达式)作为线程的入口点,然后调用join()
或detach()
来等待线程结束或者将线程分离。 -
std::mutex :
std::mutex
类用于实现互斥锁 ,防止多个线程同时访问共享资源导致数据竞争 。可以通过std::lock_guard
或std::unique_lock
来管理std::mutex
的锁定和解锁。 -
std::condition_variable :
std::condition_variable
类用于线程间的条件变量通信,一个线程可以等待另一个线程满足特定条件后再继续执行。 -
std::atomic :
std::atomic
模板类提供了原子操作,确保在多线程环境下对共享变量的操作是原子的,避免竞态条件。 -
std::future 和 std::promise :
std::future
和std::promise
用于在线程间传递异步操作的结果。std::promise
用于设置值,而std::future
用于获取这个值。
这些组件提供了丰富的功能,使得在C++程序中使用多线程变得更加容易和安全。
目录
👉🏻thread
std::thread
是 C++11 标准库中用于创建和管理线程的类。下面是 std::thread
类中一些重要的接口函数:
-
构造函数:
explicit thread(Args&&... args);
:接受线程函数参数,并在构造函数中启动新线程。
-
成员函数:
void join();
:等待线程结束,阻塞当前线程直到被调用的线程执行完毕。bool joinable() const noexcept;
:检查线程是否可加入(joinable),即线程对象是否与实际的线程相关联。void detach();
:分离线程,允许线程在后台继续执行,线程结束时自动释放资源。std::thread::id get_id() const noexcept;
:获取线程的唯一标识符。static unsigned int hardware_concurrency() noexcept;
:获取当前系统支持的并发线程数。
-
静态成员函数:
static void yield();
:提示调度器放弃当前时间片,允许其他线程执行。static void sleep_for(const std::chrono::duration& rel_time);
:使当前线程休眠一段时间。static void sleep_until(const std::chrono::time_point& abs_time);
:使当前线程休眠直到指定的时间点。
这些是 std::thread
类中最常用的接口函数,可以帮助你创建、管理和操作线程。当使用 std::thread
时,确保理解这些函数的作用和用法,以便有效地编写多线程程序。
成员函数
join() 函数:
-
函数原型:
cppvoid join();
-
参数意义 :
join()
函数没有参数。 -
返回值 :
void
-
示例代码:
cpp#include <iostream> #include <thread> void threadFunction() { std::cout << "Thread running\n"; } int main() { std::thread t(threadFunction); t.join(); // 等待线程执行完毕 std::cout << "Main thread\n"; return 0; }
在上面的示例中,
join()
函数会阻塞主线程,直到线程t
执行完毕后才继续执行主线程。
joinable()函数
-
函数原型:
cppbool joinable() const noexcept;
-
参数意义 :
joinable()
函数没有参数。 -
返回值 :
bool
,如果线程对象与实际线程相关联,则返回true
;否则返回false
。 -
示例代码:
cpp#include <iostream> #include <thread> int main() { std::thread t; std::cout << "Is thread joinable? " << (t.joinable() ? "Yes" : "No") << std::endl; t = std::thread([](){ std::cout << "Thread running\n"; }); std::cout << "Is thread joinable? " << (t.joinable() ? "Yes" : "No") << std::endl; t.join(); std::cout << "Is thread joinable? " << (t.joinable() ? "Yes" : "No") << std::endl; return 0; }
在这个示例中,先输出
No
,表示初始时线程t
不可加入;然后创建线程并输出Yes
;最后再次输出No
,表示线程在调用join()
后不可再次加入。
detach函数
-
函数原型:
cppvoid detach();
-
参数意义 :
detach()
函数没有参数。 -
返回值 :
void
-
示例代码:
cpp#include <iostream> #include <thread> void threadFunction() { std::cout << "Thread running\n"; } int main() { std::thread t(threadFunction); t.detach(); // 分离线程 // 注意:分离后不能再调用join(),否则会导致程序终止 std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待子线程执行完毕(非最佳做法) std::cout << "Main thread\n"; return 0; }
在这个示例中,线程
t
被分离后,主线程不再等待其执行完毕,而是继续执行后续代码。
get_id()函数
-
函数原型:
cppstd::thread::id get_id() const noexcept;
-
参数意义 :
get_id()
函数没有参数。 -
返回值 :
std::thread::id
,表示线程的唯一标识符。 -
示例代码:
cpp#include <iostream> #include <thread> int main() { std::thread t([]{ std::cout << "Thread ID: " << std::this_thread::get_id() << std::endl; }); std::cout << "Main thread ID: " << std::this_thread::get_id() << std::endl; t.join(); return 0; }
这个示例演示了如何使用
get_id()
函数获取线程的唯一标识符,并输出主线程和子线程的 ID。
静态成员函数
std::this_thread::yield()函数
-
函数原型:
cppvoid yield();
-
参数意义 :
yield()
函数没有参数。 -
返回值 :
void
-
功能 :
yield()
函数会将当前线程放弃处理器,以便其他线程有机会执行。调用此函数会暗示操作系统调度器立即切换到另一个可运行的线程。 -
示例代码:
cpp#include <iostream> #include <thread> void threadFunction() { for (int i = 0; i < 5; ++i) { std::cout << "Thread running\n"; std::this_thread::yield(); // 放弃处理器 } } int main() { std::thread t(threadFunction); for (int i = 0; i < 5; ++i) { std::cout << "Main thread\n"; std::this_thread::yield(); // 放弃处理器 } t.join(); return 0; }
在这个示例中,
yield()
函数被用来让出处理器,使得线程间能够交替执行。
std::this_thread::sleep_for()函数
-
函数原型:
cpptemplate< class Rep, class Period > void sleep_for( const std::chrono::duration<Rep,Period>& sleep_duration );
-
参数意义 :
sleep_duration
表示休眠的时间段,可以是std::chrono::milliseconds
,std::chrono::seconds
等类型。 -
返回值 :
void
-
功能 :
sleep_for()
函数会使当前线程休眠指定的时间段。 -
示例代码:
cpp#include <iostream> #include <thread> #include <chrono> int main() { std::cout << "Main thread starts sleeping\n"; std::this_thread::sleep_for(std::chrono::seconds(3)); // 休眠3秒 std::cout << "Main thread wakes up\n"; return 0; }
在这个示例中,主线程调用
sleep_for()
函数休眠3秒后再继续执行。
std::this_thread::sleep_until()函数
-
函数原型:
cpptemplate< class Clock, class Duration > void sleep_until( const std::chrono::time_point<Clock,Duration>& sleep_time );
-
参数意义 :
sleep_time
表示将要休眠到的时间点,通常通过std::chrono::system_clock::now() + duration
来计算。 -
返回值 :
void
-
功能 :
sleep_until()
函数会使当前线程休眠直到指定的时间点。 -
示例代码:
cpp#include <iostream> #include <thread> #include <chrono> int main() { auto wakeUpTime = std::chrono::system_clock::now() + std::chrono::seconds(5); std::cout << "Main thread starts sleeping\n"; std::this_thread::sleep_until(wakeUpTime); // 休眠直到指定时间点 std::cout << "Main thread wakes up\n"; return 0; }
在这个示例中,主线程调用
sleep_until()
函数休眠直到指定的时间点后再继续执行。
为什么thread的传参里面如果想传引用一定要用std::ref?
在C++中,std::ref
是用于包装引用的模板函数,它位于<functional>
头文件中。当你需要将一个引用传递给函数或者线程时,有时候需要使用std::ref
来确保正确的引用传递语义。
通常情况下,当你将一个变量作为参数传递给函数或者线程时,会发生值复制。但是有时候你希望传递的实际上是引用本身,而不是引用指向的值。这时就可以使用std::ref
来包装引用,以便进行正确的引用传递。
举个例子,假设有一个函数void func(int& val)
接受一个整型引用作为参数,如果你想在创建线程时将某个整型变量x
的引用传递给这个函数,可以这样做:
cpp
int x = 42;
std::thread t(func, std::ref(x));
在这个例子中,std::ref(x)
将x
的引用包装起来,然后将这个包装后的引用传递给func
函数,确保了在新线程中对x
的引用传递。
介绍完ref后,我们来回答开始的问题:
在C++中,当你创建线程时,传递参数给线程的方式是通过复制参数值来实现的。如果你想传递引用而不是值,直接将引用传递给std::thread
是不安全的,因为线程的执行可能会在原始变量(引用指向的对象)被销毁之后才开始 ,导致悬空引用 或者未定义行为。
使用std::ref
包装引用的原因在于它可以延长被引用对象的生命周期。std::ref
返回一个std::reference_wrapper
对象,该对象在复制时只是简单地复制了引用,而不是引用指向的内容。这样可以确保被引用对象在线程执行期间保持有效,并在线程完成后才会被销毁。
因此,为了避免悬空引用或者未定义行为,当你想要在线程中传递引用时,必须使用std::ref
或者std::cref
来保证被引用对象的有效性,从而确保线程安全地访问被引用的对象。
👉🏻mutex
基本接口函数
当涉及到多线程编程时,std::mutex
是一个常用的互斥量类,用于保护共享资源 ,避免多个线程同时访问导致数据竞争。以下是 std::mutex
类提供的一些主要接口函数:
-
std::mutex
构造函数:-
函数原型 :
cppmutex();
-
功能:创建一个新的互斥量对象。
-
-
lock()
函数:-
函数原型 :
cppvoid lock();
-
功能:尝试锁定互斥量,如果互斥量当前没有被其他线程锁定,则当前线程将锁定互斥量;如果互斥量已经被锁定,当前线程将被阻塞直到互斥量可用。
-
-
try_lock()
函数:-
函数原型 :
cppbool try_lock();
-
功能 :尝试去锁定互斥量,如果互斥量当前没有被其他线程锁定,则当前线程将锁定互斥量并返回
true
;如果互斥量已经被锁定,try_lock()
立即返回false
而不会阻塞当前线程。
-
-
unlock()
函数:-
函数原型 :
cppvoid unlock();
-
功能:解锁互斥量,允许其他线程尝试锁定该互斥量。
-
下面是一个简单的示例,演示了如何使用 std::mutex
来保护共享资源:
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int sharedData = 0;
void updateSharedData() {
mtx.lock();
sharedData++;
std::cout << "Thread ID: " << std::this_thread::get_id() << " updated sharedData to: " << sharedData << std::endl;
mtx.unlock();
}
int main() {
std::thread t1(updateSharedData);
std::thread t2(updateSharedData);
t1.join();
t2.join();
return 0;
}
在这个示例中,两个线程 t1
和 t2
分别尝试更新 sharedData
变量,通过 std::mutex
对这一操作进行保护。这样可以确保在任一时刻只有一个线程可以访问和修改 sharedData
,避免了数据竞争问题。
lock_guard和unique_lock
std::lock_guard
和 std::unique_lock
都是 C++ 中用于管理互斥锁(mutex)的 RAII(资源获取即初始化)类,它们可以帮助确保在作用域结束时自动释放互斥锁,避免忘记手动解锁而导致的死锁等问题。
-
std::lock_guard
:std::lock_guard
是一个轻量级的互斥锁封装类,一旦被创建,它会自动锁定传入的互斥锁,并在其作用域结束时自动解锁。std::lock_guard
适用于那些在同一作用域内需要加锁和解锁的场景,它不能手动释放锁,只能在作用域结束时自动释放锁。
-
std::unique_lock
:std::unique_lock
提供了比std::lock_guard
更灵活的功能。它不仅可以管理互斥锁的锁定和解锁,还可以手动地进行锁定和解锁。std::unique_lock
支持延迟加锁和条件变量,因此在需要更灵活控制锁的场景下更为适用。
在使用这两个类时,当创建一个 std::lock_guard
或 std::unique_lock
对象时,会在构造函数中锁定传入的互斥锁,并在对象销毁时(作用域结束)自动解锁。
以下是一个简单的示例,演示了如何使用 std::lock_guard
和 std::unique_lock
来管理互斥锁:
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void workFunction() {
std::lock_guard<std::mutex> lockGuard(mtx); // 使用 std::lock_guard 自动管理锁的加锁和解锁
// 这里可以安全地访问共享资源,因为锁已经被自动加锁
std::cout << "Worker thread is processing...\n";
}
int main() {
std::thread workerThread(workFunction);
// 主线程也可以使用 std::unique_lock 手动管理锁的加锁和解锁
std::unique_lock<std::mutex> uniqueLock(mtx);
// 这里可以安全地访问共享资源,因为锁已经被手动加锁
// 在 uniqueLock 对象生命周期结束时,互斥锁会被自动解锁
workerThread.join();
return 0;
}
在这个示例中,workFunction
函数通过 std::lock_guard
管理互斥锁,而主线程通过 std::unique_lock
手动管理互斥锁
RAII
(Resource Acquisition Is Initialization)是一种重要的 C++ 编程技术,用于管理资源的分配和释放。RAII 的核心思想是:通过在对象的构造函数中获取资源(如内存、文件句柄、互斥锁等),在对象的析构函数中释放资源,从而确保资源在对象生命周期结束时被正确释放。
使用 RAII 技术可以避免资源泄漏和忘记释放资源导致的问题,使得资源的管理更加安全和简单。当对象被创建时,它自动获取资源;当对象超出作用域时,其析构函数会自动调用,确保资源被释放。这种自动化的资源管理方式使得代码更加健壮并且易于维护。
常见的使用 RAII 的情况包括:
- 使用
std::unique_ptr
、std::shared_ptr
等智能指针来管理动态分配的内存。 - 使用
std::lock_guard
、std::unique_lock
等类来管理互斥锁的加锁和解锁。 - 使用文件流对象来管理文件的打开和关闭。
- 使用自定义的 RAII 类来管理其他资源,如数据库连接、网络连接等。
RTTI(Run-Time Type Information)是一个 C++ 的特性,用于在运行时获取对象的类型信息。通过 RTTI,你可以在程序运行时查询对象的实际类型,以及确定对象是否属于某个特定的类或子类。
C++ 中的 RTTI 主要通过两个关键字来实现:
-
dynamic_cast
:用于在继承体系中进行安全的向下转换(downcast),即将基类指针或引用转换为派生类指针或引用。如果转换不安全,dynamic_cast
会返回空指针(对于指针)或抛出std::bad_cast
异常(对于引用)。 -
typeid
:用于获取对象的类型信息。通过typeid
操作符,可以获得一个指向std::type_info
的对象,从而可以比较两个对象的类型是否相同。
这些特性允许你在运行时进行类型检查和类型转换,通常用于处理多态对象、工厂模式、插件系统等场景。需要注意的是,RTTI 的使用可能会引入一些运行时开销,并且过度依赖 RTTI 也可能暗示设计上的缺陷。因此,在使用 RTTI 时需要权衡好利弊,确保它符合程序设计的需要。
总的来说,RTTI 是 C++ 提供的一种功能强大的特性,可以在某些情况下帮助你更加灵活地处理对象的类型信息,但需要慎重使用以避免过度复杂化代码结构。
👉🏻condition_variable
当涉及到多线程编程中的线程同步和通信时,std::condition_variable
是一个重要的工具,用于实现线程间的条件变量等待和通知机制。下面是 std::condition_variable
类提供的一些主要接口函数:
-
std::condition_variable
构造函数:-
函数原型 :
cppcondition_variable();
-
功能:创建一个新的条件变量对象。
-
-
wait()
函数:-
函数原型 :
cppvoid wait(std::unique_lock<std::mutex>& lock);
-
功能 :当前线程等待条件变量,同时释放互斥锁
lock
,使得其他线程可以获取互斥锁并修改共享数据。当收到通知后,wait()
函数会重新获取互斥锁lock
并继续执行。
-
-
notify_one()
函数:-
函数原型 :
cppvoid notify_one();
-
功能:唤醒等待在条件变量上的一个线程,如果有多个线程在等待,则只会唤醒其中一个线程。
-
-
notify_all()
函数:-
函数原型 :
cppvoid notify_all();
-
功能:唤醒等待在条件变量上的所有线程。
-
下面是一个简单的示例,演示了如何使用 std::condition_variable
进行线程同步和通信:
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void workerThread() {
std::unique_lock<std::mutex> lock(mtx);
while (!ready) {
cv.wait(lock);
}
std::cout << "Worker thread is processing...\n";
}
int main() {
std::thread worker(workerThread);
// 模拟一些工作的完成
std::this_thread::sleep_for(std::chrono::seconds(2));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
std::cout << "Main thread notifies worker thread\n";
}
cv.notify_one();
worker.join();
return 0;
}
在这个示例中,主线程通过设置 ready
为 true
,并调用 notify_one()
唤醒等待在条件变量 cv
上的工作线程。工作线程在收到通知后被唤醒,然后开始处理任务。
当谈到条件变量在多线程中的作用时,我们可以通过一个幽默的例子来理解它的用处。
假设有一个办公室里有两个员工:小明和小红。他们的工作是协作完成一份文件,小明负责写文档,小红负责审核文档。他们需要保持协调,即小明写完了文档,小红要及时审核,并且在小红审核完后,小明要能够继续写新的文档。
这里,小明和小红就相当于两个线程,他们需要协作完成工作,而条件变量就是用来协调这种工作流程的。具体来说:
- 小明写完文档后,他会等待条件变量,即等待小红的审核通知。
- 小红审核完文档后,她会通知小明,同时唤醒小明继续写新的文档。
这个例子中,小明和小红就像是两个线程,他们通过条件变量来同步工作,保证了写和审核工作的顺利进行,避免了资源的浪费和不必要的等待。同时,这个例子也生动地展示了条件变量在多线程场景下的作用,希望能够帮助你更好地理解条件变量的使用。
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长