关于c#:如何正确读取Interlocked.Increment’ed int字段?

How to correctly read an Interlocked.Increment'ed int field?

假设我有一个非易失性int字段和一个Interlocked.Increment s线程。 另一个线程可以安全地直接读取此内容,还是读取也需要互锁?

我以前认为我必须使用互锁读取来确保我看到的是当前值,因为毕竟该字段不是可变的。 我一直在使用Interlocked.CompareExchange(int, 0, 0)来实现。

但是,我偶然发现了这个答案,这表明实际上普通读取总是会看到Interlocked.Increment ed值的当前版本,并且由于int读取已经是原子的,因此无需执行任何特殊操作。 我还发现了一个请求,其中Microsoft拒绝了对Interlocked.Read(ref int)的请求,进一步表明这是完全多余的。

所以我真的可以安全地读取没有Interlockedint字段的最新值吗?


如果要保证另一个线程将读取最新值,则必须使用Thread.VolatileRead()。 (*)

读取操作本身是原子性的,因此不会造成任何问题,但是如果没有volatile读取,您可能会从缓存中获取旧值,或者编译器可能会优化您的代码并完全消除读取操作。从编译器的角度来看,代码在单线程环境中工作就足够了。易失性操作和内存障碍用于限制编译器优化和重新排序代码的能力。

有几个参与者可以更改代码:编译器,JIT编译器和CPU。哪一个表明您的代码已损坏并不重要。唯一重要的是.NET内存模型,因为它指定了所有参与者必须遵守的规则。

(*)Thread.VolatileRead()并没有真正获得最新值。它将读取该值,并在读取后添加一个内存屏障。第一个易失性读取可能会获取缓存的值,而第二个易失性读取可能会获取更新的值,因为如果需要的话,第一个易失性读取的内存屏障已强制进行缓存更新。实际上,编写代码时,此细节并不重要。


有点元问题,但是关于使用Interlocked.CompareExchange(ref value, 0, 0)的一个好的方面(忽略了明显的缺点,即读取时很难理解),无论int还是long,它都可以工作。的确,int读取始终是原子的,但是long读取不是或可能不是,这取决于体系结构。不幸的是,Interlocked.Read(ref value)仅在value类型为long时有效。

考虑您从int字段开始的情况,这使得不可能使用Interlocked.Read(),因此您将直接读取该值,因为无论如何这都是原子的。但是,在开发的后期,您或其他人决定需要long-编译器不会警告您,但是现在您可能会遇到一个细微的错误:不再保证读取访问权限是原子的。我发现在这里使用Interlocked.CompareExchange()是最好的选择。根据底层处理器指令,它可能会更慢,但从长远来看,它会更安全。我对Thread.VolatileRead()的内部知识了解不够。关于此用例,它可能会"更好",因为它提供了更多的签名。

我不会尝试在循环或任何紧的方法调用内直接读取值(即没有上述任何机制),因为即使写入是易失性和/或内存障碍,也没有任何内容告诉编译器该字段的值实际上可以在两次读取之间改变。因此,该字段应为volatile或应使用任何给定的构造。

我的两分钱。


您是正确的,您不需要特殊的指令即可自动读取32位整数,但是,这意味着您将获得"整个"值(即,您不会一次写入而又一次写入)。您无法保证一旦阅读该值就不会更改。

此时,您需要确定是否需要使用其他同步方法来控制访问,例如是否要使用此值从数组中读取成员,等等。

简而言之,原子性可确保操作完全且不可分割地进行。给定某些包含N步骤的操作A,如果您在A之后进行操作,则可以确保所有N步骤都与并发操作隔离进行。

如果您有两个执行原子操作A的线程,可以保证您只会看到两个线程之一的完整结果。如果要协调线程,则可以使用原子操作来创建所需的同步。但是原子操作本身并不能提供更高级别的同步。 Interlocked系列方法可用于提供一些基本的原子操作。

同步是一种更广泛的并发控制,通常围绕原子操作构建。大多数处理器都包括内存屏障,可让您确保所有高速缓存行都被清空并且您拥有一致的内存视图。易失性读取是确保对给定存储位置进行一致访问的一种方法。

尽管不是立即适用于您的问题,但阅读有关数据库的ACID(原子性,一致性,隔离性和持久性)可能会帮助您使用术语。


是的,您阅读的所有内容都是正确的。 Interlocked.Increment的目的是使对字段进行更改时正常读取不会为假。读字段不是危险的,写字段是危险的。