原语并发读写的线程安全性

本文关键字:线程 安全性 读写 并发 原语 | 更新日期: 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