并发操作的同步

前面学习了如何保护线程间的共享数据。然而,有时候我们不仅需要保护共享数据,还需要令独立线程上的行为同步。例如,某线程只有先等另一线程的任务完成,才可以执行自己的任务。一般而言,线程常常需要等待特定事件的发生,或等待某个条件成立。只要设置一个“任务完成”的标志,或者利用共享数据存储一个类似的标志,通过定期查验该标志就可以满足需求,但这远非理想方法

上述线程间的同步操作很常见,C++标准库专门为之提供了处理工具:条件变量(conditional variable)和future

条件变量(conditional variable)

设想你坐夜行列车外出。如果要保证在正确的站点下车,一种方法是彻夜不眠,留心列车停靠的站点,那样就不会错过。可是,到达时你可能会精神疲倦。或者,你可以查看时刻表,按预定到达时间提前设定闹钟,随后安心入睡。这种方法还算管用,一般来说,你不会误站。但若列车晚点,你反而会太早醒来;也可能不巧,闹钟的电池刚好耗尽,结果你睡过头而错过下车站点。最理想的方法是,安排人员或设备,无论列车在什么时刻抵达目的站点,都可以将你唤起,那么你大可“高枕无忧”

同样的,如果线程甲要等待线程乙完成任务,可以采取集中不同的方式

方式一:在共享数据内部维护一标志(受互斥保护),线程乙完成任务后,就设置标志成立

该方式存在双重浪费:线程甲须不断查验标志,浪费原本有用的处理时间;另外,一旦互斥被锁住,则其他任何线程无法再加锁。这两点都是线程甲的弊病:如果它正在运行,就会限制线程乙可用的算力;还有,线程甲每次查验标志,都要锁住互斥以施加保护,那么,若线程乙恰好同时完成任务,也意欲设置标志成立,则无法对互斥加锁。这就像是你整晚熬夜,不停地与列车司机攀谈,于是他不得不放慢车速,因为你老使他走神,结果列车晚点。类似地,线程甲白白耗费了计算资源,它们本来可用于系统中的其它线程,最终导致毫无必要的等待时间

方式二:让线程甲调用 std:this_thread:sleep for( )函数,在各次查验之间短期休眠

bool flag;
mutex m;
void wait_for_flag()
{unique_lock<mutex> lk(m);while(!flag){lk.unlock();this_thread::sleep_for(chrono::milliseconds(100));lk.lock();}
}

上面的代码在每轮循环中,先将互斥解锁,随之休眠,再重新加锁,从而其它线程有机会获取锁,得以设置标志成立

这确有改进,因为线程休眠,所以处理时间不再被浪费。然而,休眠期的长短却难以预知。休眠期太短,线程仍会频繁查验,虚耗处理时间;休眠期太长,则令线程过度休眠。如果线程乙完成了任务,线程甲却没有被及时唤醒,就会导致延迟。过度休眠很少直接影响普通程序的运作。但是,对于高速视频游戏,过度休眠可能会造成丢帧;对于实时应用,可能会使某些时间片计算超时

方式三:使用C++标准库的工具等待事件发生

以上述甲、乙两线程的二级流水线模式为例,若数据要先进行前期处理,才可以开始正式操作,那么线程甲则需等待线程乙完成并且触发事件,其中最基本的方式是条件变量。按照“条件变量”的概念,若条件变量与某一事件或某一条件关联,一个或多个线程就能以其为依托,等待条件成立。当某线程判定条件成立时,就通过该条件变量,知会所有等待的线程,唤醒它们继续处理