原语并发读写的线程安全性
本文关键字:线程 安全性 读写 并发 原语 | 更新日期: 2023-09-27 18:19:40
- 下面的简化说明,.NET是如何处理这种情况的
- 如果这会导致问题,我是否必须锁定/门控对每个字段/属性的访问,这些字段/属性有时可能会被写入+从不同的线程访问
某个地方的田地
public class CrossRoads(){
public int _timeouts;
}
背景线程编写器
public void TimeIsUp(CrossRoads crossRoads){
crossRoads._timeouts++;
}
可能同时,尝试在其他地方阅读
public void HowManyTimeOuts(CrossRoads crossRoads){
int timeOuts = crossRoads._timeouts;
}
简单的答案是,如果从多个线程同时访问,上面的代码可能会导致问题。
.Net框架提供了两种解决方案:互锁和线程同步。
对于简单的数据类型操作(即int),使用Interlocked类的互锁将正确工作,并且是推荐的方法。
事实上,interlocked提供了特定的方法(递增和递减),使该过程变得简单:
在CrossRoads类中添加IncrementCount方法:
public void IncrementCount() {
Interlocked.Increment(ref _timeouts);
}
然后从你的后台工作人员那里调用:
public void TimeIsUp(CrossRoads crossRoads){
crossRoads.IncrementCount();
}
除非是32位操作系统上的64位值,否则读取该值是原子的。有关更多详细信息,请参阅Interlocked.Read方法文档。
对于类对象或更复杂的操作,您将需要使用线程同步锁定(C#中的锁定或VB.Net中的SyncLock)
这是通过在要应用锁的级别(例如,在类内部)创建一个静态同步对象,获得该对象的锁,并在该锁内执行(仅)必要的操作来实现的:
private static object SynchronizationObject = new Object();
public void PerformSomeCriticalWork()
{
lock (SynchronizationObject)
{
// do some critical work
}
}
好消息是,对int的读取和写入保证是原子的,因此没有撕裂的值。然而,不能保证进行安全的++,读取可能会缓存在寄存器中。还有指令重新排序的问题。
我会使用:
Interlocked.Increment(ref crossroads._timeouts);
对于写入,这将确保不会丢失任何值,以及;
int timeouts = Interlocked.CompareExchange(ref crossroads._timeouts, 0, 0);
对于读取,因为这与增量遵循相同的规则。严格来说,"波动性"可能已经足够阅读了,但人们对它的理解太差了,以至于Interlocked似乎(IMO)更安全。不管怎样,我们都在躲避锁。
好吧,我不是C#开发人员,但这是它在这个级别上的典型工作方式:
.NET是如何处理这种情况的?
解锁。不太可能保证是原子的。
我是否必须锁定/屏蔽对每个字段/属性的访问,这些字段/属性有时可能会被写入+从不同线程访问?
是的。另一种选择是为客户端提供对象的锁,然后告诉客户端在使用实例时必须锁定对象。这将减少锁定获取的数量,并保证您的客户的状态更加一致、可预测。
忘记dotnet。在机器语言级别,crossRoads._timeouts++
将被实现为INC [memory]
指令。这被称为"读取-修改-写入"指令。这些指令对于单个处理器上的多线程是原子指令*(本质上是用时间切片实现的),但对于使用多个处理器或多个核的多线程不是原子指令。
因此:
如果可以保证只有TimeIsUp()
会修改crossRoads._timeouts
,并且可以保证只有一个线程会执行TimeIsUp()
,那么这样做是安全的。TimeIsUp()
中的写作会很好,HowManyTimeOuts()
(以及其他任何地方)中的阅读也会很好。但是,如果您也在其他地方修改crossRoads._timeouts
,或者如果您再生成一个后台线程编写器,您将遇到麻烦。
在任何一种情况下,我的建议都是谨慎行事并锁定它
(*)对于单处理器上的多线程,它们是原子性的,因为线程之间的上下文切换发生在周期性中断上,而在x86体系结构上,这些指令对于中断是原子性,这意味着如果在CPU执行此类指令时发生中断,则中断将等待直到指令完成。对于更复杂的指令,例如带有REP前缀的指令,这种情况并不成立。
尽管int
可能是CPU的"本机"大小(一次处理32或64位),但如果您从不同线程读取和写入同一变量,最好锁定该变量并同步访问。
从来没有保证读取/写入可能是原子的int
。
您也可以在此处使用Interlocked.Increment
。