标准库多线程编程 – 同步问题

DinS          Written on 2017/9/5

本文介绍如何使用标准库处理线程同步问题,这一块有难度,确保你阅读了《标准库多线程编程 – 互斥问题》和《Windows多线程编程 – 同步问题》。

一、线程通信问题之同步问题

同步是线程间通信不可避免的问题,windows提供了event和semaphore。
c++则有些奇怪了,互斥有专门的头文件<mutex>,但是同步却没有所谓的<synchron>。
但是确实可以用c++实现同步,用到的是<mutex>和<condition_variable>配合

首先来看一个问题

没错,这个问题跟《Windows多线程编程 – 同步问题》中的一样。我们开辟10个线程,想让线程按照顺序依次输出编号。在代码中使用了mutex,然而互斥在这个问题上不管用,仅仅能够保证cout不被打乱。
实际上互斥只能够做到让多个线程不同时执行某段代码,但是对不同线程执行的先后顺序无能为力,这个问题必须使用同步来解决。
先看一下有问题的输出(当然某些情况下输出确实是按照顺序来的,但是不能保证每次都按顺序)

下面有请condition_variable出场。

二、condition_variable初探

先要吐槽一下,这个名字太怪了,中文翻译成条件变量,英文太长,以后都叫cv好了。

cv的原理是利用unique_lock。它可以对一个unique_lock等待,线程会进入阻塞状态,直到另一线程的同一个cv发来一个消息,这边才会继续执行。
比如可以这么写:

主要的函数就是wait和notify,当然都有扩展形式,稍后再讲。但是这么写会陷入死锁,为什么呢?

根据cv原理,只有在收到notify后wait才会通过,但是这里开辟的所有线程上来就wait,没有人能够执行到notify,自然卡死了。

使用cv需要一些技巧,常用的套路是跟一个全局标志位配合,再使用while来判断。
我们可以把代码改造如下:

稍微解释一下,定义了一个原子标志位atm_ready,在线程中使用while来判断标志位的情况。
假设现在第一个线程达到这里,判断while条件为false,直接跳过wait,并把标志位更换。现在其他线程过来了,就进入cv的wait。
第一个线程执行完后,更换标志位,然后通知其他所有等待的线程。接下来的故事就是相同的了。

强调一下为什么用while不用if。
if只判断一次,但是如果有多个线程,大家都在wait,一旦收到notify_all,所有人都继续执行,这样失去意义了。
使用while的话,所有线程收到notify_all后,再次判断是否要wait,这样才能达到目的。

说了那么半天,你这时应该反应过来:确实这样成功使用了cv,但是有个毛用!
确实啊,这样的使用方式其实退化为unique_lock了,对同步没有任何作用

一次输出结果。为之奈何?

三、condition_variable再探

还是使用cv,但是标志位要改成整型,我们这样改造代码:

多次输出结果均为:

成功了!接下来解释一下为什么成功了。
我们使用了三个全局变量来达到控制目的:mutex/cv/标志位。
在线程中的while使用了线程编号与标志位判断来决定是否进入wait。由于标志位是递增的,并且增加完再通知其他线程,所以可能的顺序只能是0、1、2…
如此完成了按顺序输出。

这个套路非常常见,以至于专门有一个简写形式。
看如下代码:

我们使用了wait的条件式。

其中lambda可能有人不太熟悉,解释一下。
第一个[]是捕获列表,里面填&相当于自动捕获需要的变量并且是引用捕获。
第二个()是传入参数,这里不填,在调用泛型算法时一般这里要填内容。
{}里是函数体,这里就是简单的判断,用到的两个变量由于自动捕获了可以直接使用。
整个wait表达的是线程直到轮到自己编号之前一直等待。
多次运行结果均如下:

再稍微讲一讲扩展形式。
除了基本的wait外还有wait_for和wait_until。
wait_for就是等一段时间,第二个参数为duration,第三个参数为predicate。
wait_until就是等到某个时间点,第二个参数为time_point,第三个参数为predicate。
跟之前遇到的大同小异。
除了notify_all还有notify_one,通知某个线程,不过这个one是随机的,用处不大。

上面说的都是condition_variable,其实c++还提供了一个condition_varibale_any。唯一区别就是cv构造时只能使用unique_lock<mutex>的对象,而cva可以使用任意类型锁的unique_lock对象。

四、小结

c++提供的同步解法和windows的有明显差异,这里非常有必要对比一下。
首先看windows,其提供了事件和信号量两种手段,事件利用触发和未触发状态来同步,信号量功能更强大,还有一个计数系统。
c++则使用cv,而且需要配合mutex和一个全局变量共同完成同步。
以上是基本回顾,下面评价一下:

1.使用便捷度

总体而言windows更简便,但是繁琐的点不一样。
windows集中在初始化和销毁对象,以及使用时繁琐的参数。
c++虽然没有上述问题,但是需要搭建一个“三件套”,并且要借助unique_lock,共同配合完成同步工作。

2.使用情况

平心而论,c++使用起来比windows难。
windows不管是事件还是同步,使用的逻辑很简单,要么set/wait,要么release/wait。
c++也是两句,notify无所谓,主要是wait的条件设定比较复杂,需要使用到谓词,通常是lambda。也就是说为了用好wait你需要掌握其他的c++高级知识。不过这样的付出是有回报的,要知道windows的事件和信号量要么是bool要么是计数,但是c++通过谓词却提供了复杂条件判断的可能,这个在复杂同步问题上能够发挥奇效。

3.效率

从测试上看不出什么明显的区别。
从理论上来说,事件的效率应该最高,信号量由于是内核对象所以用到的东西多一些。
c++的条件变量本身应该不费什么,但是要配合mutex/atomic/lambda,用到的东西自然多了。

至此同步问题结束。c++提供了一个很好玩的东西用于处理异步问题,感兴趣的可见《c++异步调用机制》。

 

参考资料:http://www.cnblogs.com/haippy/p/3284540.html