《C++并发编程实战》精读总结:第四章 并发操作的同步
之前我们实现的是对公共数据的保护,但是线程之间几乎没有其他任何的交互操作。像“某线程需要等待另一线程完成后,才能开始执行”这种行为,我们通常称为线程的同步。本章主要是来展示线程是如何进行同步(交互)的。
1. 条件等待——condition_variable
假设有两个线程,线程1准备数据并发出,线程2在线程1准备好数据发出之后,拿到数据再进行一定的操作。这里就符合线程2必须等待线程1将数据发出之后才能进行操作的逻辑。借助condition_variable可以实现这种有条件的等待。线程1在准备好所有之后,调用condition_variable的nofity_one或者notify_all函数。线程2则调用condition_variable的wait函数。代码如下:
mutex mutex1;
queue<Data> que; // 公共数据,需要被锁住的
condition_variable cond;
void thread1(){
Data data1;
// 一旦线程1准备好数据data1,就锁住互斥
lock_guard<mutex> lk(mutex1);
// 将数据压入公共队列que
que.push(data1);
// 完成压入后,调用cond的notify_one()函数,通知线程2
cond.notify_one();
// cond.notify_all();
}
void thread2(){
// 线程2在等待期间,要解锁互斥,在等待结束后,要重新加锁,只有unique_lock能提供这种灵活性
unique_lock<mutex> lk(mutex1);
// 线程2在cond的wait函数上,传入锁lk和一个lambda函数
// 如果函数成立,wait返回,否则wait解锁lk,并阻塞当前线程2
cond.wait(lk,[]{return ! que.empty();});
Data data2 = que.front();
que.pop();
lk.unlock(); // 数据2就绪,没必要继续锁住互斥,可以释放
process(data2);
}
其中,如果有多个线程等待,就使用notify_all()函数,本例只有一个线程等待,所以thread1中用nofity_one()即可。因为有共享的队列,所以在执行过程中,需要用到互斥和加锁。thread2中的wait函数,一个参数是互斥锁,第二个是一个判断函数。如果函数成立,那么进行thread2中的后续操作,如果函数不成立,wait会释放互斥,让整个thread2进入阻塞或等待。为了灵活的加锁和释放锁,thread2中使用unique_lock来对互斥加锁。
2. 等待一次性时间发生——future
2.1 从后台任务返回值——async
如果需要等待的事件只会发生一次,可以将这个事件用future来表示。一旦等待的这个事件发生,future就会进入就绪状态,无法重置。简单的代码如下:
#include <future>
#include <iostream>
using namespace std;
int FindAnswer(){}
int main(){
future<int> answer = async(launch::async,FindAnswer); // FindAnswer被异步执行,是否并发取决于编译器
do_other_stuff();
cout << "answer is: " << answer.get(); // get会阻塞当前线程,等待FindAnswer执行完成
}
需要注意的是,主线程是可以在FindAnswer异步执行的时候,去做自己的事儿(do_other_stuff),但是当调用future的get函数时,还是会阻塞当前主线程,等待FindAnwser执行完,或者已经执行完的话就直接返回结果。另外async作为典型的异步执行的方式,第一个参数还以选择launch::deferred,这样就是不开启另外一个线程,等待主线程执行到调用future的get函数,再开始执行FindAnswer。
2.2 隐藏函数细节,打包future——packaged_task<>
将任务函数和future打包到一起,就是packaged_task。打包到一起的好处时可以隐藏函数的细节,直接通过管理task来管理不同的线程任务,这在多线程任务中比较方便。具体代码如下:
mutex m;
deque<packaged_task<void()>> tasks; // 共享的队列
template<typename Func>
void post_task(Func f){
packaged_task<void()> task(f); // 根据传入的函数创建任务,将任务包装在task里
// future<void> res=task.get_future(); // 调用成员函数取得对应的future
lock_guard<mutex> lk(m);
tasks.push_back(move(task));
}
void execute_task(){
packaged_task<void()> task;
lock_guard<mutex> lk(m);
task=move(tasks.front());
tasks.pop_front();
task(); //相当于执行了future.get()
}