并发场景下,有时我们并不仅仅想保护数据,我们还希望多个线程之间同步某些操作,例如等待某个条件为真或者某个事件发生时执行一些操作。C++标准库提供了条件变量(condition variables)和futures;并发技术规范(Concurrency Technical Specification (TS))提供了latches和barriers。
标准库中提供两种条件变量:std::condition_variable和std::condition_variable_any,它们都必须与互斥锁配合使用,以提供同步功能,而前者必须与std::mutex使用,后者可以与满足互斥锁最小要求的互斥锁类配合使用,由于通用性,其性能和开销会较大。
#include <condition_variable> #include <queue> #include <iostream> #include <thread> std::mutex mut; std::queue<int> data_queue; std::condition_variable data_cond; int prepare_data() { return rand() % 100; } void process(int data) { std::cout << "process data: " << data << " remain: " << data_queue.size() << std::endl; } bool is_last_chunk(int data) { return data == 0; } void data_preparation_thread() { //srand仅对当前线程有效 srand((unsigned int)time(nullptr)); while (true) { const int data = prepare_data(); { std::lock_guard<std::mutex> lk(mut);//1 data_queue.push(data); }//3 data_cond.notify_one();//4 if (is_last_chunk(data)) break; } } void data_processing_thread() { while (true) { std::unique_lock<std::mutex> lk(mut);//2 data_cond.wait( lk, [] { return !data_queue.empty(); }); int data = data_queue.front(); data_queue.pop(); lk.unlock(); process(data);//5 if (is_last_chunk(data)) break; } } int main() { std::thread thread1(data_preparation_thread); std::thread thread2(data_processing_thread); thread1.join(); thread2.join(); }
这里模拟了一个线程生产数据,另一个线程取数据的场景,生产的数据是一系列数组,0代表数据结束标记。
显然取数据和放数据都要先获取互斥锁(1,2)。对生产数据线程直接使用lock_guard最方便,我们通过一个单独的作用域使得在3位置互斥锁被释放。push数据后通过条件变量的notify_one方法通知等待当前条件的线程,
对处理数据线程,我们使用了unique_lock而不是lock_guard,这是因为unique_lock可以手动unlock和lock,这不仅有利于提高效率(处理数据时没必要持有锁5),也是条件变量wait方法的要求:wait方法传入两个参数,第一个参数是一个unique_lock,第二个参数是可调用对象,表示等待的条件。进行条件判断前先获取互斥锁,然后执行可调用对象,如果返回false,则释放锁,继续等待;如果返回true,继续持有锁,直到unlock或者离开作用域自动释放锁。所谓“等待”是指线程进入阻塞或者等待状态。当其他线程在相同条件变量调用了notify_one,等待的线程再次进入获取锁并判断是否满足条件的流程,不满足则继续等待,满足则往下执行。
注意,因为条件检测可能执行很多次,因此传入的函数或可调用对象不能有副作用
通过运行程序,发现并非每次notify_one都会使得等待线程苏醒,获取这是因为notify_one的频率过快?或者跟时间片有关?或者出于效率考虑?参考输出如下:
process data: 92 remain: 12 process data: 41 remain: 25 process data: 93 remain: 24 process data: 95 remain: 23 process data: 39 remain: 22 process data: 68 remain: 21 process data: 82 remain: 20 process data: 74 remain: 19 process data: 95 remain: 18 process data: 38 remain: 17 process data: 26 remain: 16 process data: 81 remain: 15 process data: 98 remain: 14 process data: 39 remain: 13 process data: 95 remain: 12 process data: 7 remain: 11 process data: 32 remain: 10 process data: 48 remain: 9 process data: 48 remain: 8 process data: 20 remain: 7 process data: 89 remain: 6 process data: 12 remain: 5 process data: 20 remain: 4 process data: 43 remain: 3 process data: 27 remain: 2 process data: 9 remain: 1 process data: 0 remain: 0