使用多线程总会伴随 数据的并发访问。多个线程彼此毫无关系地运行几乎是很罕见的。线程可能提供数据给其他线程处理,或是准备好必要的先决条件用以启动其他线程。当多个线程同时运行的时候一定要注意当多个线程并发处理相同的数据而不同步化,那么唯一安全的情况就是所有线程只读取数据而不修改数据。
所谓相同的数据是使用相同的内存区,如果在不同的内存区那么就不会引发数据竞争的问题,读写也就不会出问题。在C++11中定义的数据竞争是指不同线程中的两个互相冲突的动作,其中至少一个动作不是atomic,而且无一个动作发生在另外一个动作之前。Data race会导致程序不可预期的行为。使用多线程的时候要特别注意平台相关性。
在什么情况下会出现问题呢?
1.未同步化的数据访问 :并行运行的两个线程渡河写同一个数据,不知道哪条语句先执行
2.写一半的数据:某个线程正在读数据,另一个线程改动它,于是读取数据的线程可能只读一半
3.重新安装的语句: 语句和操作可能被重新安排次序,也许对每个单一线程正确,但是多线程的组合就破坏了预期的行为。
Unsynchronized data Access(未同步化的数据访问)
1 | std::vector<int> v; |
如果此段代码被多个线程共享,那么有可能在调用if非空后成功,而其他线程操作之后将v中元素清空,那么此段函数导致不可预期的行为。
注意:C++标准库提供的函数不支持“写或读”动作与另外一个”写“动作(同一个数据)并发执行。但是并发处理同意容器内的不同元素是可以的(除vector
Half-Written Data(写到一半的数据)
如下:
1 | int x = 0; |
某个线程写入:
1 | x=-1; |
某个线程读取:
1 | std::cout << x; |
程序的输出可能有以下情况:
x = 0 : 如果在第一线程执行之前
x=-1 : 如果写入线程已完成
任何其他值: 如果 读取线程在写入线程中进行读取。
Reordered Statement(重排语句)
假设有两个变量,一个是bool readyFlag,另外一个是int data。在一个线程中表示提供数据:
1 | int data; |
在生成者线程中:
1 | data = 100; |
消费者线程中:
1 | while(!readFlag){} |
一般而言是先执行生成者线程产生数据,然后执行消费者线程处理数据。但是编译器或者硬件可能重排语句指令,从而在生成者为完成数据产生之前调用foo(data)语句,而不是执行while循环,对于单一线程则不会出席这种问题。
1 | foo(data); |
这就有可能造成不可预期的行为。
如何解决?
首先要保证代码的原子性(atomicity),即不可分割性,同时也要保证具体指定语句的次序(Order)。
1.使用future和promise,它们都保证atomicity和order,这就意味着不会同时发生读和写
2.使用mutex和lock来处理critical section和protected zone。获得锁即保证只有一方可以读或者写。但是如果锁使用的不当,会造成死锁。
3.可以使用condition_variable,有效的令某线程等待若干“被另外一个线程控制的判断式”为true。有利于处理多线程直接的次序关系。
4.可以使用atomic data type确保每次对便利或对象的访问操作都是不可分割的。
5.可以使用atomic data type的底层接口,允许放宽atomic语句的次序或针对内存访问使用manual barrier或fence.