我有多个线程使用“++”或“--”操作访问单个 Int32 变量。
我们是否需要在访问它之前锁定,如下所示?
lock (idleAgentsLock)
{
idleAgents--; //we should place it here.
}
或者不加锁会有什么后果?
它不是“原子的”,也不是多线程意义上的。但是,您的锁至少在理论上可以保护操作。如有疑问,您当然可以Interlocked.Decrement
改用。
否 ++/-- 不是原子操作,但是读写整数和其他原始时间被认为是原子操作。
请参阅以下链接:
这个博客帖子的方式也会让你感兴趣。
我建议Interlocked类中的Interlocked.Increment用于 ++/-- 运算符的原子实现。
这取决于机器架构。一般来说,不,编译器可能会生成加载指令,递增/递减值,然后存储它。因此,其他线程可能确实会读取这些操作之间的值。
大多数 CPU 指令集为此目的都有一个特殊的原子测试和设置指令。假设您不想将汇编指令嵌入到 C# 代码中,下一个最佳方法是使用互斥,类似于您所展示的。该机制的实现最终使用原子指令来实现互斥锁(或任何它使用的)。
简而言之:是的,您应该确保互斥。
除了这个答案的范围之外,还有其他管理共享数据的技术可能合适或不合适,具体取决于您的情况的域逻辑。
不受保护的增量/减量不是线程安全的- 因此线程之间不是原子的。(虽然它可能是“原子的” wrt 实际的 IL/ML 转换1。)
此 LINQPad 示例代码显示了不可预测的结果:
void Main()
{
int nWorkers = 10;
int nLoad = 200000;
int counter = nWorkers * nLoad;
List<Thread> threads = new List<Thread>();
for (var i = 0; i < nWorkers; i++) {
var th = new Thread((_) => {
for (var j = 0; j < nLoad; j++) {
counter--; // bad
}
});
th.Start();
threads.Add(th);
}
foreach (var w in threads) {
w.Join();
}
counter.Dump();
}
请注意,线程之间的可见性很重要。除了原子性之外,同步还保证了这种可见性。
至少在所提供的有限上下文中,此代码很容易修复。关闭减量并观察结果:
counter--; // bad
Interlocked.Decrement(ref counter); // good
lock (threads) { counter--; } // good
1即使使用volatile
变量,结果仍然无法预测。这似乎表明(至少在这里,当我刚刚运行它时)它也不是原子运算符,因为竞争线程的读/操作/写是交错的。要查看删除可见性问题后行为仍然不正确(是吗?),添加
class x {
public static volatile int counter;
}
并修改上面的代码以使用x.counter
而不是局部counter
变量。