【问题标题】:locking only 1 operation?只锁定1个操作?
【发布时间】:2012-05-14 08:03:54
【问题描述】:

我一直在问自己:“为什么我应该只对一个语句使用锁定”...

(恕我直言 - 如果它的 1 操作只是像赋值 - 所以应该没有问题..)?

然后我看到了这个:

作为基本规则,您需要锁定访问任何可写共享 场地。即使在最简单的情况下——对单个的赋值操作 字段—您必须考虑同步。在接下来的课程中, Increment 和 Assign 方法都不是线程安全的:

class ThreadUnsafe
{
  static int _x;
  static void Increment() { _x++; }  
  static void Assign() { _x = 123; }
}

你能告诉我为什么这不是线程安全的吗? 我在脑海中运行了许多脚本,但找不到任何问题...

【问题讨论】:

  • Increment 不安全(3 次操作)。 Assign 也是不安全的(1 个原子操作,但您需要通过缓存处理内存同步)。
  • @Soohjun 增量是原子操作(INC dword ptr [ebp-3Ch])
  • @YoryeNathan 这不是原子的。见MSDN。
  • 如果你不想在你的单个语句周围加锁,你可以使用内置的联锁类来为你做这件事,例如static void Increment() { Interlocked.Increment(ref _x);}
  • @YoryeNathan 您误解了“原子”的含义。这并不意味着“一个 CPU 指令”。例如,INC dword ptr [ebp-3Ch] 是但原子的一切。它本质上是进行加载、公司、存储。

标签: c# .net multithreading .net-4.0


【解决方案1】:

这是一个示例,说明您的示例为何不是线程安全的。最初,_x = 0。假设您并行运行Increment 和Assign。如果方法是线程安全的,则结果应该是100(如果在分配之前执行增量)或101(如果在分配之后执行增量)。

(编辑:请注意,每个线程都有自己的工作堆栈!)

 Thread 1 (executing Increment)    Thread 2 (executing Assign 100)
 -----------------------------------------------------------------
 read _x onto stack       (= 0)
                                   put 100 on top of stack
                                   write top of stack to _x (= 100)
 increment top of stack   (= 1)
 write top of stack to _x (= 1)

_x 现在是 1,既不是 100 也不是 101。

当然,您的增量方法可能被编译器编译为单个原子操作。但是你不能依赖这个,除非你使用的编译器特别保证。


如果使用锁,会发生以下情况:

 Thread 1 (executing Increment)    Thread 2 (executing Assign 100)
 -----------------------------------------------------------------
 lock (success)
 read _x onto stack       (= 0)
                                   lock (lock already taken; 
                                   |     wait until Thead 1's lock is released)
 increment top of stack   (= 1)    |
 write top of stack to _x (= 1)    |
 unlock                            |
                                   +> (success)
                                   put 100 on top of stack
                                   write top of stack to _x (= 100)
                                   unlock

结果现在是100。基本上,锁确保两个锁定的块不重叠。

【讨论】:

  • 我第二个@RoyiNamir。增量将导致 101,其中预期为 1 - 这基本上是问题所在。
  • @RoyiNamir:我已经扩展了示例以使其更加清晰。每个线程都有自己的工作堆栈。
  • 如果你使用锁,你会得到 100 或 101,而不是 1。
  • @RoyiNamir:不,锁将确保线程 1 上的所有三个操作都执行线程 2 不会中断。我已经扩展了我的答案。
  • 但是如果每个都有自己的堆栈,为什么还要中断呢?
【解决方案2】:

递增操作产生这个 MSIL...

.method private hidebysig static void  Increment() cil managed
{
  // Code size       14 (0xe)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldsfld     int32 ThreadUnsafe::_x
  IL_0006:  ldc.i4.1
  IL_0007:  add
  IL_0008:  stsfld     int32 ThreadUnsafe::_x
  IL_000d:  ret
} // end of method ThreadUnsafe::Increment

因此您可以看到,即使在 MSIL 级别,增量也不是原子的。 JIT 编译器可能会做一些聪明的事情来将其转回机器级别的原子增量,但我们当然不能依赖它。想象一下 2 个线程在“加载”和“存储”操作重叠的情况下递增相同的 X - 您可以看到最终可能是 X = X + 1 而不是 X + 2。

将增量包裹在锁内意味着它们不能重叠。

【讨论】:

    【解决方案3】:

    你必须在比编程语言更低的层次上思考。

    不能保证

    a) 处理器将一次性写入新值(原子或非原子)

    b) 该值将在一个 CPU 内核的缓存中更新,但不会在另一个 CPU 内核中更新(缺少内存屏障)

    也许您的 CPU(很可能)可以原子地读取和写入 32 位整数,您不会有任何问题。但是当你试图读/写一个 64 位值时会发生什么? 128?该值可能最终处于不确定状态,其中两个不同的线程同时修改相同的内存位置,并且您最终得到值 a、值 b,或者是两者混合的中间值(并且非常不正确)。

    还有更多。

    【讨论】:

    • 实际上 C# 保证分配给 Int32 是原子的。
    • 根据参考规范 int 更新是原子的,但是寄存器缓存的远程可能性。
    • @Soohjun 但不一定是通过内存屏障,我想?这就留下了缓存一致性的问题。
    【解决方案4】:

    锁定是一个非常混乱的主题,您通常很难弄清楚引擎盖下发生了什么(哪个核心缓存何时失效)。这就是为什么编写高效的并行代码是一个问题。其他人甚至指出了一些潜在的问题,即使是单个赋值(显然是增加一个变量)。只需查看volatile 关键字的所有问题:https://www.google.com/search?q=.net+volatile+concurrency&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:en-US:official&client=firefox-a

    因此,如果您必须并行执行操作,请从锁定很多开始,即使在您认为不需要锁定的操作上也是如此。仅在遇到性能问题时才优化锁定。

    【讨论】:

      猜你喜欢
      • 2017-07-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-13
      • 1970-01-01
      • 1970-01-01
      • 2012-02-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多