C++11多线程库
C++11
中提供了多线程的标准库,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。
线程库简介
Linux下使用std::thread
C++11以来,C++引入了标准线程库std::thread
。标准线程库的实现由各平台自行决定。在C++有标准线程库之前,Linux下已经存在了一个广受好评(或者说,不得不用)的一个线程库,pthread。所以Linux上的std::thread
其实就是对之前存在的pthread的一层包装。 Linux下一般使用的C++实现是libstdc++,由于设计原因,线程库以单独的libpthread提供,并且libstdc++使用弱链接的方式引用libpthread
因此在Linux下要么使用thread.h(pthread)
,要么添加额外的编译选项,将生成目标链接到pthread库
CMake中,可以使用
set_target_properties(${TARGET} PROPERTIES
COMPILE_FLAGS "-pthread"
LINK_FLAGS "-pthread")
的方式,强制为编译和链接增加选项-pthread
。注意这部分代码不能用
target_link_libraries(${TARGET} pthread)
代替,因为后者会扩展为-lpthread
,并且仅对链接阶段生效。
这样的方法不适合跨平台的使用,并且在目标比较多的时候,添加起来比较麻烦。CMAKE中提供了单独的Threads
库来解决这个问题。
add_library(test ${src})
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
target_link_libraries(test PUBLIC Threads::Threads)
这里我们将目标test
对线程库Threads::Threads
的链接属性设置为PUBLIC
,这样随后如果有目标静态链接 test
或者动态链接test
,都可以隐式地加入对Threads::Threads
的依赖。那些不直接依赖动态链接库,而是使用dlopen()
的方式打开动态链接库的目标,则必须手动添加对Threads::Threads
的依赖。
-lpthread和-pthread的区别
编译程序包括 预编译, 编译,汇编,链接,包含头文件了,仅能说明有了线程函数的声明, 但是还没有实现, 加上-lpthread是在链接阶段,链接这个库。
<stdio.h>等都是静态库,不需要做额外的表示,连接时会直接链接进代码里。pthread是动态库,需要用-lpthread,所有的动态库都需要用-lxxx来引用
用gcc编译使用了POSIX thread的程序时通常需要加额外的选项,以便使用thread-safe的库及头文件,一些老的书里说直接增加链接选项 -lpthread 就可以了
而gcc手册里则指出应该在编译和链接时都增加 -pthread 选项
编译选项中指定 -pthread 会附加一个宏定义 -D_REENTRANT,该宏会导致 libc 头文件选择那些thread-safe的实现;链接选项中指定 -pthread 则同 -lpthread 一样,只表示链接 POSIX thread 库。由于 libc 用于适应 thread-safe 的宏定义可能变化,因此在编译和链接时都使用 -pthread 选项而不是传统的 -lpthread 能够保持向后兼容,并提高命令行的一致性。
目前gcc 4.5.2中已经没有了关于 -lpthread的介绍了。所以以后的多线程编译应该用-pthread,而不是-lpthread。
创建线程
多线程库对应的头文件是#include <thread>
,类名为std::thread
。
一个简单的串行程序如下:
//c++11多线程 std::thread
#include <thread>
#include <iostream>
void f1()
{
std::cout << "f1" << std::endl;
}
int main(int argc, char *argv[])
{
f1();
return 0;
}
这是一个典型的单线程的单进程程序,任何程序都是一个进程,main()
函数就是其中的主线程,单个线程都是顺序执行。
将上面的程序改造成多线程程序其实很简单,让f1()
函数在另外的线程中执行:
void f1()
{
std::cout << "f1" << std::endl;
}
int main(int argc, char *argv[])
{
std::thread t1(f1);
t1.join();
return 0;
}
- 首先,构建一个
std::thread
对象t1
,构造的时候传递了一个参数,这个参数是一个函数,这个函数就是这个线程的入口函数,函数执行完了,整个线程也就执行完了。 - 线程创建成功后,就会立即启动,并没有一个类似
start
的函数来显式的启动线程。 - 一旦线程开始运行, 就需要显式的决定是要等待它完成(join),或者分离它让它自行运行(detach)。注意:只需要在
std::thread
对象被销毁之前做出这个决定。这个例子中,对象t1
是栈上变量,在main
函数执行结束后就会被销毁,所以需要在main
函数结束之前做决定。 - 这个例子中选择了使用
t1.join()
,主线程会一直阻塞着,直到子线程完成,join()
函数的另一个任务是回收该线程中使用的资源。
线程对象和对象内部管理的线程的生命周期并不一样,如果线程执行的快,可能内部的线程已经结束了,但是线程对象还活着,也有可能线程对象已经被析构了,内部的线程还在运行。
假设t1
线程是一个执行的很慢的线程,主线程并不想等待子线程结束就想结束整个任务,直接删掉t1.join()
是不行的,程序会被终止(析构t1
的时候会调用std::terminate
,程序会打印terminate called without an active exception
)。
与之对应,我们可以调用t1.detach()
,从而将t1
线程放在后台运行,所有权和控制权被转交给C++
运行时库,以确保与线程相关联的资源在线程退出后能被正确的回收。参考UNIX
的守护进程(daemon process)的概念,这种被分离的线程被称为守护线程(daemon threads)。线程被分离之后,即使该线程对象被析构了,线程还是能够在后台运行,只是由于对象被析构了,主线程不能够通过对象名与这个线程进行通信。例如:
//c++11多线程 std::thread
#include <thread>
#include <iostream>
void f1()
{
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "f1" << std::endl;
}
void test()
{
std::thread t1(f1);
t1.detach();
//t1.join();
std::cout << "test finished" << std::endl;
}
/**************
* join与detach的区别
* join阻塞等待线程执行完毕
* detach守护进程放到后台运行
* join与detach只能二选一,后面也不能互相调用
* 可以使用joinable()函数判断一个线程对象能否调用join()。
* *****************/
int main(int argc, char *argv[])
{
test();
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
return 0;
}
// 使用 t1.detach()时
// test() finished
// f1
// 使用 t1.join()时
// f1
// test() finished
-
由于线程入口函数内部有个
500ms
的延时,所以在还没有打印的时候,test()
已经执行完成了,t1
已经被析构了,但是它负责的那个线程还是能够运行,这就是detach()
的作用。 -
如果去掉
main
函数中的1s
延时,会发现什么都没有打印,因为主线程执行的太快,整个程序已经结束了,那个后台线程被C++
运行时库回收了。 -
如果将
t1.detach()
换成t1.join()
,test
函数会在t1
线程执行结束之后,才会执行结束。一旦一个线程被分离了,就不能够再被
join
了。如果非要调用,程序就会崩溃,可以使用joinable()
函数判断一个线程对象能否调用join()
。
void test() {
std::thread t1(function_1);
t1.detach();
if(t1.joinable())
t1.join();
assert(!t1.joinable());
}
thread构造函数
std::thread
类的构造函数是使用可变参数模板实现的,也就是说,可以传递任意个参数,第一个参数是线程的入口函数,而后面的若干个参数是该函数的参数。
第一参数的类型并不是c
语言中的函数指针(c
语言传递函数都是使用函数指针),在c++11
中,增加了**可调用对象(Callable Objects)**的概念,总的来说,可调用对象可以是以下几种情况:
- 函数指针
- 重载了
operator()
运算符的类对象,即仿函数 lambda
表达式(匿名函数)std::function
函数指针
// 普通函数 无参
void function_1() {
}
// 普通函数 1个参数
void function_2(int i) {
}
// 普通函数 2个参数
void function_3(int i, std::string m) {
}
std::thread t1(function_1);
std::thread t2(function_2, 1);
std::thread t3(function_3, 1, "hello");
t1.join();
t2.join();
t3.join();
如果将重载的函数作为线程的入口函数,会发生编译错误!编译器搞不清楚是哪个函数:
// 普通函数 无参
void function_1() {
}
// 普通函数 1个参数
void function_1(int i) {
}
std::thread t1(function_1);
t1.join();
// 编译错误
/*
C:\Users\Administrator\Documents\untitled\main.cpp:39:
error: no matching function for call to 'std::thread::thread(<unresolved overloaded function type>)'
std::thread t1(function_1);
^
*/
仿函数
// 仿函数
class Fctor {
public:
// 具有一个参数
void operator() () {
}
};
Fctor f;
std::thread t1(f);
// std::thread t2(Fctor()); // 编译错误
std::thread t3((Fctor())); // ok
std::thread t4{Fctor()}; // ok
一个仿函数类生成的对象,使用起来就像一个函数一样,比如上面的对象f
,当使用f()
时就调用operator()
运算符。所以也可以让它成为线程类的第一个参数,如果这个仿函数有参数,同样的可以写在线程类的后几个参数上。
而t2
之所以编译错误,是因为编译器并没有将Fctor()
解释为一个临时对象,而是将其解释为一个函数声明,编译器认为你声明了一个函数,这个函数不接受参数,同时返回一个Factor
对象。解决办法就是在Factor()
外包一层小括号()
,或者在调用std::thread
的构造函数时使用{}
,这是c++11
中的新的同意初始化语法。
但是,如果重载的operator()
运算符有参数,就不会发生上面的错误。
匿名函数
std::thread t1([](){
std::cout << "hello" << std::endl;
});
std::thread t2([](std::string m){
std::cout << "hello " << m << std::endl;
}, "world");
std::function
class A{
public:
void func1(){
}
void func2(int i){
}
void func3(int i, int j){
}
};
A a;
std::function<void(void)> f1 = std::bind(&A::func1, &a);
std::function<void(void)> f2 = std::bind(&A::func2, &a, 1);
std::function<void(int)> f3 = std::bind(&A::func2, &a, std::placeholders::_1);
std::function<void(int)> f4 = std::bind(&A::func3, &a, 1, std::placeholders::_1);
std::function<void(int, int)> f5 = std::bind(&A::func3, &a, std::placeholders::_1, std::placeholders::_2);
std::thread t1(f1);
std::thread t2(f2);
std::thread t3(f3, 1);
std::thread t4(f4, 1);
std::thread t5(f5, 1, 2);
传值还是引用
如果线程入口函数的的参数是引用类型,在线程内部修改该变量,主线程的变量会改变吗?
#include <iostream>
#include <thread>
#include <string>
// 仿函数
class Fctor {
public:
// 具有一个参数 是引用
void operator() (std::string& msg) {
msg = "wolrd";
}
};
int main() {
Fctor f;
std::string m = "hello";
std::thread t1(f, m);
t1.join();
std::cout << m << std::endl;
return 0;
}
// g++编译器: 编译报错
std::thread
类,内部也有若干个变量,当使用构造函数创建对象的时候,是将参数先赋值给这些变量,所以这些变量只是个副本,然后在线程启动并调用线程入口函数时,传递的参数只是这些副本,所以内部怎么操作都是改变副本,而不影响外面的变量。
而如果可以想真正传引用,可以在调用线程类构造函数的时候,用std::ref()
包装一下:
std::thread t1(f, std::ref(m));
当然这样并不好,多个线程同时修改同一个变量,会发生数据竞争。
同理,构造函数的第一个参数是可调用对象,默认情况下其实传递的还是一个副本。
#include <iostream>
#include <thread>
#include <string>
class A {
public:
void f(int x, char c) {}
int g(double x) {return 0;}
int operator()(int N) {return 0;}
};
void foo(int x) {}
int main() {
A a;
std::thread t1(a, 6); // 1. 调用的是 copy_of_a()
std::thread t2(std::ref(a), 6); // 2. a()
std::thread t3(A(), 6); // 3. 调用的是 临时对象 temp_a()
std::thread t4(&A::f, a, 8, 'w'); // 4. 调用的是 copy_of_a.f()
std::thread t5(&A::f, &a, 8, 'w'); //5. 调用的是 a.f()
std::thread t6(std::move(a), 6); // 6. 调用的是 a.f(), a不能够再被使用了
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
t6.join();
return 0;
}
对于线程t1
来说,内部调用的线程函数其实是一个副本,所以如果在函数内部修改了类成员,并不会影响到外面的对象。只有传递引用的时候才会修改。所以在这个时候就必须想清楚,到底是传值还是传引用!
只能移动不可复制
线程对象之间是不能复制的,只能移动,移动的意思是,将线程的所有权在std::thread
实例间进行转移。
void some_function();
void some_other_function();
std::thread t1(some_function);
// std::thread t2 = t1; // 编译错误
std::thread t2 = std::move(t1); //只能移动 t1内部已经没有线程了
t1 = std::thread(some_other_function); // 临时对象赋值 默认就是移动操作
std::thread t3;
t3 = std::move(t2); // t2内部已经没有线程了
t1 = std::move(t3); // 程序将会终止,因为t1内部已经有一个线程在管理了
互斥锁mutex
并发代码中最常见的错误之一就是竞争条件(race condition)。而其中最常见的就是数据竞争(data race),从整体上来看,所有线程之间共享数据的问题,都是修改数据导致的,如果所有的共享数据都是只读的,就不会发生问题。但是这是不可能的,大部分共享数据都是要被修改的。
众所周知,cout是线程不安全的(缓冲区),尽量使用printf
mutex
使用std::mutex
进行资源保护,头文件是#include <mutex>
,共有两种操作:锁定(lock)与解锁(unlock)。
解决办法就是要对cout
这个共享资源进行保护。在c++
中,可以使用互斥锁std::mutex
进行资源保护,头文件是#include <mutex>
,共有两种操作:锁定(lock)与解锁(unlock)。将cout
重新封装成一个线程安全的函数:
#include <mutex>
//互斥锁mutex
void shared_mutex()
{
std::mutex lock;
lock.lock();
for (int i = 0; i < 10; i++)
{
std::cout << "shared_mutex " << i << std::endl;
}
lock.unlock();
}
int main()
{
std::thread t3(shared_mutex);
for (int i = 0; i < 10; i++)
{
std::cout << "main" << i << std::endl;
}
t3.join();
}
使用std::mutex
声明锁并加锁解锁即可。如果mu.lock()
和mu.unlock()
之间的语句发生了异常,会发生什么?unlock()
语句没有机会执行!导致导致mu
一直处于锁着的状态,其他使用shared_print()
函数的线程就会阻塞。
解决这个问题也很简单,使用c++
中常见的RAII
技术,即**获取资源即初始化(Resource Acquisition Is Initialization)**技术,**这是c++
中管理资源的常用方式。简单的说就是在类的构造函数中创建资源,在析构函数中释放资源,因为就算发生了异常,c++
也能保证类的析构函数能够执行。**我们不需要自己写个类包装mutex
,c++
库已经提供了std::lock_guard
类模板
std::lock_guard
锁管理遵循RAII习语来处理资源。锁管理器在构造函数中自动绑定它的互斥体,并在析构函数中释放它。这大大减少了死锁的风险,因为运行时会处理互斥体。
锁管理器在C++ 11中有两种:用于简单的std::lock_guard,以及用于高级用例的std::unique_lock。
使用std::lock_guard:
{
std::mutex m,
std::lock_guard<std::mutex> lockGuard(m);
sharedVariable= getVar();
}
注意开括号 { 和闭括号 },保证std::lock_guard生命周期只在这{}里面有效。当生命周期离开临界区时,它的生命周期就结束了。
函数体范围或循环范围也限制了对象的生命周期。
void shared_print(string msg, int id) {
//构造的时候帮忙上锁,析构的时候释放锁
std::lock_guard<std::mutex> guard(mu);
//mu.lock(); // 上锁
cout << msg << id << endl;
//mu.unlock(); // 解锁
}
可以实现自己的std::lock_guard
,类似这样:
class MutexLockGuard
{
public:
explicit MutexLockGuard(std::mutex& mutex)
: mutex_(mutex)
{
mutex_.lock();
}
~MutexLockGuard()
{
mutex_.unlock();
}
private:
std::mutex& mutex_;
};
std::unique_lock
lock_guard
只能保证在析构的时候执行解锁操作,lock_guard
本身并没有提供加锁和解锁的接口
但是有些时候会有这种需求。看下面的例子。
class LogFile {
std::mutex _mu;
ofstream f;
public:
LogFile() {
f.open("log.txt");
}
~LogFile() {
f.close();
}
void shared_print(string msg, int id) {
{
std::lock_guard<std::mutex> guard(_mu);
//do something 1
}
//do something 2
{
std::lock_guard<std::mutex> guard(_mu);
// do something 3
f << msg << id << endl;
cout << msg << id << endl;
}
}
};
上面的代码中,一个函数内部有两段代码需要进行保护,这个时候使用lock_guard
就需要创建两个局部对象来管理同一个互斥锁(其实也可以只创建一个,但是锁的力度太大,效率不行),修改方法是使用unique_lock
。它提供了lock()
和unlock()
接口,能记录现在处于上锁还是没上锁状态,在析构的时候,会根据当前状态来决定是否要进行解锁(lock_guard
就一定会解锁)。上面的代码修改如下:
class LogFile {
std::mutex _mu;
ofstream f;
public:
LogFile() {
f.open("log.txt");
}
~LogFile() {
f.close();
}
void shared_print(string msg, int id) {
std::unique_lock<std::mutex> guard(_mu);
//do something 1
guard.unlock(); //临时解锁
//do something 2
guard.lock(); //继续上锁
// do something 3
f << msg << id << endl;
cout << msg << id << endl;
// 结束时析构guard会临时解锁
// 这句话可要可不要,不写,析构的时候也会自动执行
// guard.ulock();
}
};
上面的代码可以看到,在无需加锁的操作时,可以先临时释放锁,然后需要继续保护的时候,可以继续上锁,这样就无需重复的实例化lock_guard
对象,还能减少锁的区域。同样,可以使用std::defer_lock
设置初始化的时候不进行默认的上锁操作:
void shared_print(string msg, int id) {
std::unique_lock<std::mutex> guard(_mu, std::defer_lock);
//do something 1
guard.lock();
// do something protected
guard.unlock(); //临时解锁
//do something 2
guard.lock(); //继续上锁
// do something 3
f << msg << id << endl;
cout << msg << id << endl;
// 结束时析构guard会临时解锁
}
这样使用起来就比lock_guard
更加灵活!然后这也是有代价的,因为它内部需要维护锁的状态,所以效率要比lock_guard
低一点,在lock_guard
能解决问题的时候,就是用lock_guard
,反之,使用unique_lock
。
后面在学习条件变量的时候,还会有unique_lock
的用武之地。
另外,请注意,unique_lock
和lock_guard
都不能复制,lock_guard
不能移动,但是unique_lock
可以!
// unique_lock 可以移动,不能复制
std::unique_lock<std::mutex> guard1(_mu);
std::unique_lock<std::mutex> guard2 = guard1; // error
std::unique_lock<std::mutex> guard2 = std::move(guard1); // ok
// lock_guard 不能移动,不能复制
std::lock_guard<std::mutex> guard1(_mu);
std::lock_guard<std::mutex> guard2 = guard1; // error
std::lock_guard<std::mutex> guard2 = std::move(guard1); // error
使用锁封装资源
使用锁将数据封装成一个类。由于cout
是个全局共享的变量,没法完全封装,就算你封装了,外面还是能够使用cout
,并且不用通过锁。下面使用文件流举例:
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
using namespace std;
std::mutex mu;
class LogFile {
std::mutex m_mutex;
ofstream f;
public:
LogFile() {
f.open("log.txt");
}
~LogFile() {
f.close();
}
void shared_print(string msg, int id) {
std::lock_guard<std::mutex> guard(mu);
f << msg << id << endl;
}
};
void function_1(LogFile& log) {
for(int i=0; i>-100; i--)
log.shared_print(string("From t1: "), i);
}
int main()
{
LogFile log;
std::thread t1(function_1, std::ref(log));
for(int i=0; i<100; i++)
log.shared_print(string("From main: "), i);
t1.join();
return 0;
}
上面的LogFile
类封装了一个mutex
和一个ofstream
对象,然后shared_print
函数在mutex
的保护下,是线程安全的。使用的时候,先定义一个LogFile
的实例log
,主线程中直接使用,子线程中通过引用传递过去(也可以使用单例来实现),这样就能保证资源被互斥锁保护着,外面没办法同时使用资源。
但是这个时候还是得小心了!用互斥元保护数据并不只是像上面那样保护每个函数,就能够完全的保证线程安全,如果将资源的指针或者引用不小心传递出来了,所有的保护都白费了!要记住一下两点:
- 不要提供函数让用户获取资源。
std::mutex mu;
class LogFile {
std::mutex m_mutex;
ofstream f;
public:
LogFile() {
f.open("log.txt");
}
~LogFile() {
f.close();
}
void shared_print(string msg, int id) {
std::lock_guard<std::mutex> guard(mu);
f << msg << id << endl;
}
// Never return f to the outside world
ofstream& getStream() {
return f; //never do this !!!
}
};
- 不要资源传递给用户的函数。
class LogFile {
std::mutex m_mutex;
ofstream f;
public:
LogFile() {
f.open("log.txt");
}
~LogFile() {
f.close();
}
void shared_print(string msg, int id) {
std::lock_guard<std::mutex> guard(mu);
f << msg << id << endl;
}
// Never return f to the outside world
ofstream& getStream() {
return f; //never do this !!!
}
// Never pass f as an argument to user provided function
void process(void fun(ostream&)) {
fun(f);
}
};
以上两种做法都会将资源暴露给用户,造成不必要的安全隐患。
设计安全的数据处理过程
需要注意的是,即使每个函数都是线程安全的,但是组合起来不一定是线程安全的,因为每个函数内部的锁只保护函数内部的数据处理过程,对于全局变量/数据来说,其仍然被多线程操作,没有被同步
//互斥锁mutex
void shared_mutex()
{
std::mutex lock;
lock.lock();
for (int i = 0; i < 10; i++)
{
std::cout << "shared_mutex " << i << std::endl;
}
lock.unlock();
}
//使用模板std::lock_guard<std::mutex> 自动管理和释放锁
void mutex_lock_guard()
{
std::mutex lock;
{
std::lock_guard<std::mutex> lockGuard(lock);
for (int i = 0; i < 10; i++)
{
std::cout << "mutex_lock_guard " << i << std::endl;
}
}
}
//竞态条件,t3 t4是线程安全的,但组合起来却不是
std::thread t3(shared_mutex);
std::thread t4(mutex_lock_guard);
for (int i = 0; i < 10; i++)
{
std::cout << "main " << i << std::endl;
}
死锁
这里的死锁通常是代码块里用到了多个互斥锁,例如fun1里运行了fun2,fun1与fun2都用到了全局锁,多线程执行时导致死锁
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <fstream>
using namespace std;
class LogFile {
std::mutex _mu;
std::mutex _mu2;
ofstream f;
public:
LogFile() {
f.open("log.txt");
}
~LogFile() {
f.close();
}
void shared_print(string msg, int id) {
std::lock_guard<std::mutex> guard(_mu);
std::lock_guard<std::mutex> guard2(_mu2);
f << msg << id << endl;
cout << msg << id << endl;
}
void shared_print2(string msg, int id) {
std::lock_guard<std::mutex> guard(_mu2);
std::lock_guard<std::mutex> guard2(_mu);
f << msg << id << endl;
cout << msg << id << endl;
}
};
void function_1(LogFile& log) {
for(int i=0; i>-100; i--)
log.shared_print2(string("From t1: "), i);
}
int main()
{
LogFile log;
std::thread t1(function_1, std::ref(log));
for(int i=0; i<100; i++)
log.shared_print(string("From main: "), i);
t1.join();
return 0;
}
std::lock
c++
标准库中提供了std::lock()
函数,能够保证将多个互斥锁同时上锁
std::lock(_mu, _mu2);
同时,lock_guard
也需要做修改,因为互斥锁已经被上锁了,那么lock_guard
构造的时候不应该上锁,只是需要在析构的时候释放锁就行了,使用std::adopt_lock
表示无需上锁:
std::lock_guard<std::mutex> guard(_mu2, std::adopt_lock);
std::lock_guard<std::mutex> guard2(_mu, std::adopt_lock);
也就是要么同时拿到两把锁,要么都拿不到
互斥锁std::mutex
是一种最常见的线程间同步的手段,但是在有些情况下不太高效。
条件变量(Condition Variable)
实现一个简单的消费者生产者模型,一个线程往队列中放入数据,一个线程往队列中取数据,取数据前需要判断一下队列中确实有数据,由于这个队列是线程间共享的,所以,需要使用互斥锁进行保护,一个线程在往队列添加数据的时候,另一个线程不能取,反之亦然。
c++11
中提供了#include <condition_variable>
头文件,其中的std::condition_variable
可以和std::mutex
结合一起使用,其中有两个重要的接口,notify_one()
和wait()
,wait()
可以让线程陷入休眠状态,在消费者生产者模型中,如果生产者发现队列中没有东西,就可以让自己休眠,但是不能一直不干活啊,notify_one()
就是唤醒处于wait
中的其中一个条件变量(可能当时有很多条件变量都处于wait
状态)。那什么时刻使用notify_one()
比较好呢,当然是在生产者往队列中放数据的时候了,队列中有数据,就可以赶紧叫醒等待中的线程起来干活了。
/**
*
* 生产者消费者模型
*
**/
#include <iostream>
#include <thread>
#include <deque>
#include <mutex>
#include <condition_variable>
std::deque<int> q;
std::mutex mu;
std::condition_variable condition;
const static int MAX_SIZE = 10;
/**
* @brief 生产者
*
*/
void product()
{
int size = 0;
while (size < MAX_SIZE)
{
//生产者
std::unique_lock<std::mutex> lock(mu);
q.push_back(size);
lock.unlock();
condition.notify_one(); //条件变量,通知线程
std::this_thread::sleep_for(std::chrono::seconds(1));
size++;
}
}
/**
* @brief 消费者
*
*/
void consumer()
{
int data = 0;
while (data < MAX_SIZE)
{
std::unique_lock<std::mutex> lock(mu);
while (q.empty())
{
condition.wait(lock); //lock-mu 等待被唤醒
}
data = q.front();
q.pop_front();
lock.unlock();
std::cout << data << std::endl;
}
}
int main(int argc, char **argv)
{
std::thread t1(product);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
- 在
consumer
中,在判断队列是否为空的时候,使用的是while(q.empty())
,而不是if(q.empty())
,这是因为wait()
从阻塞到返回,不一定就是由于notify_one()
函数造成的,还有可能由于系统的不确定原因唤醒(可能和条件变量的实现机制有关),这个的时机和频率都是不确定的,被称作伪唤醒,如果在错误的时候被唤醒了,执行后面的语句就会错误,所以需要再次判断队列是否为空,如果还是为空,就继续wait()
阻塞。 - 在管理互斥锁的时候,使用的是
std::unique_lock
而不是std::lock_guard
,而且事实上也不能使用std::lock_guard
,这需要先解释下wait()
函数所做的事情。可以看到,在wait()
函数之前,使用互斥锁保护了,如果wait
的时候什么都没做,岂不是一直持有互斥锁?那生产者也会一直卡住,不能够将数据放入队列中了。所以,**wait()
函数会先调用互斥锁的unlock()
函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作。**而lock_guard
没有lock
和unlock
接口,而unique_lock
提供了。这就是必须使用unique_lock
的原因。 - 使用细粒度锁,尽量减小锁的范围,在
notify_one()
的时候,不需要处于互斥锁的保护范围内,所以在唤醒条件变量之前可以将锁unlock()
。
还可以将cond.wait(locker);
换一种写法,wait()
的第二个参数可以传入一个函数表示检查条件,这里使用lambda
函数最为简单,如果这个函数返回的是true
,wait()
函数不会阻塞会直接返回,如果这个函数返回的是false
,wait()
函数就会阻塞着等待唤醒,如果被伪唤醒,会继续判断函数返回值。
std::unique_lock<std::mutex> locker(mu);
cond.wait(locker, [](){ return !q.empty();} ); // Unlock mu and wait to be notified
除了notify_one()
函数,c++
还提供了notify_all()
函数,可以同时唤醒所有处于wait
状态的条件变量。
std::this_thread
std::this_thread是c++11提供的辅助函数类,封装了一些有用的函数
get_id: 获取线程 ID。
std::thread::id this_id = std::this_thread::get_id();
yield: 当前线程放弃执行,操作系统调度另一线程继续执行。
sleep_for: 线程休眠某个指定的时间片(time span),该线程才被重新唤醒,不过由于线程调度等原因,实际休眠时间可能比 sleep_duration 所表示的时间片更长。
std::chrono::milliseconds dura( 2000 );
std::this_thread::sleep_for( dura );
sleep_until: 线程休眠至某个指定的时刻(time point),该线程才被重新唤醒。
template< class Clock, class Duration >
void sleep_until( const std::chrono::time_point<Clock,Duration>& sleep_time );