仅锁定1次操作?

时间:2012-05-14 08:03:55

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

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

(恕我直言 - 如果它的1次操作只是作为一项任务 - 所以不应该有问题......)?

然后我看到了这个:

  

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

class ThreadUnsafe
{
  static int _x;
  static void Increment() { _x++; }  
  static void Assign() { _x = 123; }
}
你可以告诉我为什么这不是线程安全的吗? 我一直在运行许多脚本,无法找到任何问题......

4 个答案:

答案 0 :(得分:4)

以下是您的示例不是线程安全的示例。最初,_x = 0。假设您并行运行IncrementAssign。如果方法是线程安全的,结果应该是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。基本上,锁定确保两个锁定的块不重叠。

答案 1 :(得分:4)

增量操作产生这个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结束。

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

答案 2 :(得分:1)

你必须考虑比编程语言更低的水平。

无法保证

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

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

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

还有更多。

答案 3 :(得分:0)

锁定是一个非常混乱的主题,你通常会非常难以弄清楚引擎盖下的内容(当核心缓存失效时)。这就是为什么编写高效的并行代码是一个问题。其他人已指出一些潜在的问题,即使只有一个任务(显然增加一个变量)。只需查看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

所以,如果你必须并行做事,首先要锁定很多,即使是你不认为需要锁定的操作。只有在您发现性能问题时才能优化锁定。