同步条件变量:等待多次事件
在c++多线程中,我们学习了用各种方法去保护在线程间共享的数据,但有时我们不只是需要保护数据,还需要在独立的线程上进行同步操作。例如一个线程在完成其任务之前需要等待另一个线程完成任务,c++标准库便提供了以条件变量和期值为形式的工具来处理它。
考虑下面一种状况:如果一个线程正在等待第二个线程完成一项任务,他有几个选择?
首先,他可以一直检查共享数据(由互斥元保护)中的标识,并且让第二个线程在完成任务时设置该标识。但这会造成两项浪费,线程占据了宝贵的处理时间去反复检查该标识,以及当互斥元被等待的线程锁定后,就不能被任何其他线程锁定。两者都对线程进行等待,因为他们限制了等待中的线程的可用资源,甚至阻止它在完成任务时设置标识。同时也因为等待中的线程消耗了本可以被系统中其他线程使用的资源,导致最终的等待的时间可能会更长。
第二个选择,我们可以设置一个休眠时间,让等待中的线程休眠一会:
bool flag; std::mutex mtx; void wait_for_falg(){ std::unique_lock lk(mtx); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); lk.lock(); }
在上面的代码中,函数在休眠之前解锁互斥元,并在之后再次锁定,所以其他线程在该线程休眠期可以获取它并设置标识。
上面这种方法是个不错的选择,但它仍然有缺陷,该缺陷取决于休眠时间长短你的设定,如果设置的过短,那么线程仍然会浪费时间进行检查,如果设置的过长,线程等待的任务已经完成,他还会继续休眠,导致延迟。在网络游戏中这种操作可能会导致丢帧。
第三种选择也是首要选择,是使用c++标准库提供的工具来等待事件本身。等待由另一个线程触发一个线程的最基本机制是条件变量。
条件变量与某些事件或某些条件相关,当某个线程已经确定条件得到满足,他就可以通知一个或多个正在条件变量上进行等待的线程,以便唤醒他们并让他们继续处理。
标准库提供了两个条件变量的实现,std::condition_variable和std::condition_variable_any,这两个实现都在头文件中声明,两个都需要与互斥元一起工作,以便提供适当的同步,后者比前者适用范围更广,故会有大小、性能或者操作系统资源等方面的形式的额外代价的可能,因此如无特殊情况,首选std::condition_variable。
例:
std::mutex mtx; template std::queue data_queue; std::condition_variable data_cond; //条件变量的创建 void data_thread(){ while(something()) { data_chunk const data=get_data(); std::lock_guard lk(mtx); data_queue.push(data); data_cond.notify_one(); //条件变量的唤醒操作 //唤醒其他线程中调用wait的某一个线程 } } void data_processing_thread() { while(true){ std::unique_lock lk(mtx); data_cond.wait(lk,[]{ return !data_queue.empty(); }); data_chunk data=data_queue.front(); data_queue.pop(); lk.unlock(); do_something(); } }
上述代码主要有两块用到条件变量,一处是data_cond.notify_one(),该函数的作用是为了唤醒另一个(只能是一个)调用过wait函数的阻塞线程,
还有一处便是wait操作。wait函数有两个参数,分别是互斥元和一个预测条件,当该条件为false是阻塞该线程,并且只有当有其他线程调用notify_one()时再次检查预测条件,只有当预测条件为true时才会解除阻塞。