12.5.1并发冲突分析:
多个线程中,同时使用std::cout这个资源,就会触发资源并发访问冲突。cout代表标准输出(通常是指显示屏),多个线程同时输出,就容易造成屏幕上一片混乱。
运行结果:
例中每个并发都会执行10次这个操作:
cout << "i ==> " << i << " <~~ " << endl;
这看是一行代码,实际对应的CPU操作恐怕有上百个,至少我们语义层面划分,就可以分成四个:
① cout << "i ==> "
② cout << i
③ cout << " <~~ "
④ cout << endl
CPU会在同时运行的线程间轮换,没有一个线程可以独占CPU时间而从容地完成10次循环;甚至很难独占CPU时间而一口气完成四个步骤。很可能一个线程输出"i ==> ",正要执行第二个步骤时,另一个线程就抢了CPU然后输出" <~~ "。
语言越高级(越方便人类表达思维),它的一行语句生成的机器指令往往越多。看个实例,C++代码如下
int main()
{
int a = 1;
int b = 2;
int c;
c = a + b;
cout << c << endl;
}
主函数中前四行代码的汇编结果(为方便比较,汇编之前插入对应的源代码)是:
int a = 1;
movl $0x1, -0xc(%ebp)
int b = 2;
movl $0x2, -0x10(%ebp)
int c;
c = a + b;
mov -0xc(%ebp), %edx
mov -0x10(%ebp), %eax
add %edx, %eax
mov %eax, -0x14(%ebp)
我们可以看到 c = a + b;被编译成四行汇编语句。
某个操作有可能被打断,我们称该操作是“非原子操作”,不能或不允许被打断的操作,称为“原子操作”。
绝大多数C++写出的语句,都不是原子操作。
比如例中出现的原子操作,我们强调是“采用字面常量初始化”的代码,如果是使用旧的变量初始化新的变量(类似拷贝构造),那就变成非原子操作了:
int d = c;
mov -0x14(%ebp), %eax
mov %eax, -0x18(%ebp)
同样存在一个从源内存(变量c)复制到寄存器,在从寄存器复制到目标内存(变量d)的过程。
不仅多数C++语句操作不是原子操作,甚至连单个汇编指令也大有可能不是原子操作,因为一个指令可能需要CPU花费多个时间周期执行。
那么,能在一个时间周期内完成的指令是不是就一定是原子操作?答案是否定的,到底什么操纵是不可分割的原子操作?
对于初学者来说,干脆认定所有操作都可能被别的线程打断。
比如:
COUNT = 5; //COUNT是一个其他线程也可以修改的变量
double a = (1 / COUNT);
两行语句执行之后,a的值是多少?单线程环境下是0.2,多线程环境下有可能是其他值,不幸遇上COUNT为0,程序还会挂掉。
如果心里没有对并发下的共用资源时时刻刻留神,我们就非常容易认定002行一定会紧接着001行执行,忘了会有其他线程可能正好也在运行,并且正好也修改了COUNT的值,并且修改的时机正好位于001和002行之间。
这就涉及到并发冲突问题难搞的第二个原因:冲突并不总发生,许多时候它们隐藏的很深。
#include <iostream>
#include <future>
#include <thread>
using namespace std;
int COUNT;
void foo_1()
{
COUNT = 0;
}
int foo_2(int v)
{
COUNT = 5;
return v/COUNT;
}
int main()
{
std::thread tr(foo_1);
std::future <int> f = async(foo_2, 100);
tr.join();
cout << f.get() << endl;
return 0;
}
这段代码运行100万次,可能也不会撞上冲突,但它的冲突逻辑问题客观存在。
我们在代码中,加几行输出代码: