线程同步.为什么这个锁不足以同步线程

Val*_*zub 6 .net c# multithreading locking

可能重复:
线程同步.完全锁定如何使内存访问"正确"?

这个问题的灵感来自于这个问题.

我们有一个以下的测试课程

class Test
{
    private static object ms_Lock=new object();
    private static int ms_Sum = 0;

    public static void Main ()
    {
        Parallel.Invoke(HalfJob, HalfJob);
        Console.WriteLine(ms_Sum);
        Console.ReadLine();
    }

    private static void HalfJob()
    {
        for (int i = 0; i < 50000000; i++) {
            lock(ms_Lock) { }// empty lock

            ms_Sum += 1;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

实际结果非常接近预期值100 000 000(50 000 000 x 2,因为2个循环同时运行),差异大约为600 - 200(我的机器上的误差约为0.0004%,非常低).没有其他同步方式可以提供这种近似方式(它要么是更大的错误%,要么是100%正确)

我们目前理解这种精确程度是因为程序以下列方式运行:

在此输入图像描述

时间从左到右运行,2个线程由两行表示.

哪里

  • 黑匣子代表获取,持有和释放的过程

  • lock plus表示加法操作(架构表示我的PC上的比例,锁定大约比添加的时间长20倍)

  • 白框表示由试图获取锁定,并进一步等待它变得可用的时期

锁也提供完整的内存栅栏.

所以现在的问题是:如果上面的模式代表了正在发生的事情,那么这个大错误的原因是什么(现在它的大原因模式看起来非常强大的同步模式)?我们可以理解1-10之间的界限差异,但它显然不是错误的唯一原因吗?我们无法看到何时可以同时发生对ms_Sum的写入,从而导致错误.

编辑:很多人喜欢快速得出结论.我知道同步是什么,如果我们需要正确的结果,那么上面的构造不是真正的或接近好的同步线程的方法.对海报有一些信心,或者先阅读相关的答案.我不需要同步2个线程来并行执行添加的方法,我正在探索这种奢侈而又高效的方法,与任何可能的和近似的替代方案相比,同步构造(它在某种程度上同步,因此它不像建议的那样毫无意义)

And*_*rey 7

lock(ms_Lock) { }这是毫无意义的构造.lock保证在其中独占执行代码.

让我解释为什么这个空的lock减少(但不会消除!)数据损坏的可能性.让我们简化一下线程模型:

  1. 线程在时间片执行一行代码.
  2. 线程调度以严格的循环方式(ABAB)完成.
  3. Monitor.Enter/Exit的执行时间比算术时间长得多.(比方说说长3倍.我用Nops 填充代码表示前一行仍在执行.)
  4. 真正+=需要3个步骤.我将它们分解为原子的.

在左列显示哪条线在线程的时间片(A和B)处执行.在右栏 - 程序(根据我的模型).

A   B           
1           1   SomeOperation();
    1       2   SomeOperation();
2           3   Monitor.Enter(ms_Lock);
    2       4   Nop();
3           5   Nop();
4           6   Monitor.Exit(ms_Lock);
5           7   Nop();
7           8   Nop();
8           9   int temp = ms_Sum;
    3       10  temp++;
9           11  ms_Sum = temp;                          
    4           
10              
    5           
11              

A   B           
1           1   SomeOperation();
    1       2   SomeOperation();
2           3   int temp = ms_Sum;
    2       4   temp++;
3           5   ms_Sum = temp;                          
    3           
4               
    4           
5               
    5           
Run Code Online (Sandbox Code Playgroud)

正如您在第一个场景中看到的那样,线程B无法捕获线程A而A有足够的时间来完成执行ms_Sum += 1;.在第二种情况下,ms_Sum += 1;交错并导致持续的数据损坏.实际上,线程调度是随机的,但这意味着线程A 在另一个线程到达之前有更多的更改来完成增量.

  • 虽然在这里没有正确使用,但这并非毫无意义.它仍然会导致完整的内存屏障 - 即使不使用关键区域. (2认同)

Bra*_*vic 6

这是一个非常紧凑的循环,内部没有多少进展,因此ms_Sum += 1有可能被并行线程在"错误的时刻"执行.

你为什么要在实践中写这样的代码?

为什么不:

lock(ms_Lock) { 
    ms_Sum += 1;
}
Run Code Online (Sandbox Code Playgroud)

要不就:

Interlocked.Increment(ms_Sum);
Run Code Online (Sandbox Code Playgroud)

- 编辑---

一些评论为什么你会看到错误尽管锁的内存屏障方面...想象一下以下场景:

  • 线程A进入lock,离开lock,然后被OS调度程序抢占.
  • 线程B进入和离开lock(可能一次,可能多于一次,可能数百万次).
  • 此时,线程A再次被调度.
  • A和B都同时命中ms_Sum += 1,导致一些增量丢失(因为增量=加载+添加+存储).