假设我有一个非易失性的int字段和一个Interlocked.Increment
的线程。另一个线程可以直接安全地读取它,还是读取也需要互锁?
我之前认为我必须使用互锁读取来保证我看到当前值,因为毕竟该字段不是易失性的。我一直在使用Interlocked.CompareExchange(int, 0, 0)
来实现这一目标。
然而,我偶然发现了this answer,这表明实际的普通读取总会看到Interlocked.Increment
ed值的当前版本,并且因为int读取已经是原子的,所以没有必要这样做什么特别的。我还发现a request in which Microsoft rejects a request for Interlocked.Read(ref int),进一步表明这完全是多余的。
那么我可以在没有int
的情况下真正安全地阅读此类Interlocked
字段的最新值吗?
答案 0 :(得分:19)
如果您想保证其他线程将读取最新值,您必须使用Thread.VolatileRead()
。 (*)
读取操作本身是原子的,因此不会导致任何问题,但是如果没有易失性读取,您可能会从缓存中获得旧值,或者编译器可能会优化您的代码并完全消除读取操作。从编译器的角度来看,代码在单线程环境中工作就足够了。易失性操作和内存屏障用于限制编译器优化和重新排序代码的能力。
有几个参与者可以改变代码:编译器,JIT编译器和CPU。它们中哪一个显示您的代码被破坏并不重要。唯一重要的是 .NET内存模型,因为它指定了所有参与者必须遵守的规则。
(*)Thread.VolatileRead()
并没有真正获得最新值。它将读取该值并在读取后添加内存屏障。第一个易失性读取可能会获得缓存值,但第二个将获得更新值,因为第一个易失性读取的内存屏障在必要时强制执行缓存更新。实际上,在编写代码时,这个细节并不重要。
答案 1 :(得分:13)
一个元问题,但使用Interlocked.CompareExchange(ref value, 0, 0)
的一个好方面(忽略了用于阅读时难以理解的明显缺点)是它无论int
还是{{{{}}都有效1}}。确实long
读取始终是原子的,但int
读取不是或可能不是,具体取决于体系结构。不幸的是,long
仅在Interlocked.Read(ref value)
类型为value
时才有效。
考虑一下你从long
字段开始的情况,这使得无法使用int
,因此你将直接读取该值,因为无论如何它都是原子的。但是,在开发后期,您或其他人决定需要Interlocked.Read()
- 编译器不会警告您,但现在您可能会有一个微妙的错误:不再保证读取访问权限是原子的。我发现在这里使用long
是最好的选择;根据底层处理器指令,它可能会更慢,但从长远来看它更安全。我对Interlocked.CompareExchange()
的内部结构不太了解;对于这个用例可能“更好”,因为它提供了更多的签名。
我不会尝试在循环或任何紧密方法调用中直接读取值(即没有任何上述机制),因为即使写入是易失性和/或内存屏障,也没有任何东西告诉编译器,该字段的值实际上可以在两个读取之间进行更改。因此,该字段应该是Thread.VolatileRead()
或者应该使用任何给定的结构。
我的两分钱。
答案 2 :(得分:8)
你是正确的,你不需要一个特殊的指令来原子地读取32位整数,但是,这意味着你将获得“整体”值(即你不会得到一个写入的一部分和另一部分)。您无法保证在阅读后该值不会发生变化。
此时您需要决定是否需要使用其他同步方法来控制访问,例如,如果您使用此值来从数组中读取成员等等。
简而言之,原子性可确保操作完全且不可分割地发生。给定一些包含A
步骤的操作N
,如果您在A
之后立即执行操作,则可以确保所有N
步骤与并发操作隔离。
如果您有两个执行原子操作A
的线程,则可以保证只会看到两个线程之一的完整结果。如果要协调线程,可以使用原子操作来创建所需的同步。但原子操作本身并不能提供更高级别的同步。 Interlocked
系列方法可用于提供一些基本的原子操作。
同步是一种更广泛的并发控制,通常围绕原子操作构建。大多数处理器都包含内存屏障,允许您确保刷新所有缓存行,并且您具有一致内存视图。易失性读取是确保对给定内存位置的一致访问的一种方法。
虽然不能立即应用于您的问题,但阅读有关数据库的ACID(原子性,一致性,隔离性和持久性)可能会帮助您使用术语。
答案 3 :(得分:-6)
是的,你读过的一切都是正确的。 Interlocked.Increment的设计使得在对字段进行更改时,正常读取不会为false。读一个字段并不危险,写一个字段就是。