下面由一段代码来引申出如果没有线程同步带来的问题
static int sum = 0;
static void Main(string[] args)
{
//尽可能大一些 , 触发竞争的几率就高 多运行几次查看结果
Parallel.For(0, 100000, i => {
sum++;
});
Console.WriteLine(sum);
}
上面的代码很简单我们使用了并行库,在多线程并行执行的情况下对一个全局的静态变量进行递增操作,我们都知道在单核CPU架构上多线程的执行其实是时间片的切换,那么本质上只要 sum++ 这个语句是原子操作的话就不会有任何问题,因为在单核CPU架构上始终都只有一个线程在执行( 注:原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉)
那么 sum++ 是原子性的吗 , 从代码上看 sum++ 感觉就是原子性的 , 因为只有一句代码,代表着将 sum 所标识的内存中的值加1,然而在我们代码被编译之后我们看到从机器指令翻译的汇编代码中 sum++ 这种自增操作有会被翻译成如下常见的2种形式 (不同的编译器,不同的架构 如 x86 或 x64 , 或CPU架构都会导致编译/运行结果有差别)
第一种
mov eax,dword ptr ds:[0x13A440C] //从 sum 中取值 到 eax 寄存器中
inc eax //执行 +1 操作
mov ds:[0x13A440C],dword ptr eax //将自增后的值放回到 sum 变量中
上面的代码是c#代码被编译执行时转换成的汇编代码,可以看到 sum++ 这个操作在执行时被分成了三步,这里假设,我们的CPU是单核的 , 并且当前有2个线程同时运行这段代码 sum的初始值是0 ,线程1在运行完 inc eax 的时候 ,系统时钟引发了中断,导致线程2 开始执行, 我们来分析一下 线程1 在执行 +1
操作前首先从内存取出sum变量中存储的值,然后放到 eax 寄存器中 执行 +1
操作 ,当+1操作执行完成时 需要最后一步操作将增加的值放回到 sum变量中,这时却系统的时钟却刚好中断了导致线程2开始执行.
线程2的执行过程与线程1是一样,需要先获取值到寄存器执行 +1再放回到sum变量中,如果线程2执行完成了,此时内存中 sum 变量的值 应该是 1,而线程1在线程2执行完成后继续开始执行,此时只是执行第三句代码 也就是将 eax 的值 放回到 sum 中, 所以结果也是1
我们执行了2次sum++ , 本来sum的结果应该是 2 , 但因多线程竞争的问题导致了结果错误
第二种
inc dword ptr ds:[0x13A440C]
大家可以在VS中下断点,然后转到汇编代码调试,会看到这个过程
第二种生成的汇编代码,可以保证再单核CPU多线程的情况下不出问题,因为尽管会有线程切换的发生,但是只会发生在一句代码的执行前或执行后,而不会在执行的过程中中断执行,所以从单核CPU的角度上面这个代码不会发生问题,但是在多核CPU中仍然会发生问题,因为上面这个代码是可以被多个线程同时执行的,因此仍然会产生数据错误的现象
那么当同步问题发生后我们应该如何处理这个问题,让我们在下篇文章中与大家详细说明,大家也可以关注我的C#专题课程,在视频课程中一起学习