仅锁定 1 个操作
本文关键字:操作 锁定 | 更新日期: 2023-09-27 18:34:08
我一直在问自己:"为什么我应该只对一个语句使用锁"......
(恕我直言 - 如果它的 1 个操作只像作业一样 - 所以应该没有问题..)?
然后我看到了这个:
作为基本规则,您需要锁定访问任何可写共享 田。即使在最简单的情况下 — 对单个 字段 - 必须考虑同步。在下面的课程中, 增量和分配方法都不是线程安全的:
class ThreadUnsafe
{
static int _x;
static void Increment() { _x++; }
static void Assign() { _x = 123; }
}
你能告诉我为什么这不是线程安全的吗?我一直在脑海中运行许多脚本,找不到任何问题......
下面是一个示例,说明为什么您的示例不是线程安全的。最初,_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
.基本上,锁确保两个锁定的块不会重叠。
增量操作会产生此 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 结束。
将增量包装在锁中意味着它们不能重叠。
在比编程语言更低的水平上思考。
无法保证
a) 处理器将一次性写入所有新值(原子或非原子)
b) 该值将在一个 CPU 内核的缓存中更新,但不会在另一个 CPU 内核中更新(缺乏内存障碍)
也许您的 CPU(可能)可以原子方式读取和写入 32 位整数,您不会有任何问题。但是,当您尝试读取/写入 64 位值时会发生什么?A 128?该值可能最终处于不确定状态,其中两个不同的线程同时修改相同的内存位置,并且您最终得到值 a、值 b 或混合的中间(非常不正确)值。
等等。
锁定是一个大混乱的主题,您通常很难弄清楚引擎盖下的内容(哪个核心缓存何时失效)。这就是为什么编写高效的并行代码是一个问题。其他人指出了一些潜在的问题,即使是单个赋值(显然是递增变量)。只需查看易失性关键字的所有问题: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
因此,如果您必须并行执行操作,请从锁定大量开始,即使在您认为不需要锁定的操作上也是如此。仅当您发现性能问题时,才优化锁定。