假设我有一个非易失性int字段和一个Interlocked.Increment s线程。 另一个线程可以安全地直接读取此内容,还是读取也需要互锁?
我以前认为我必须使用互锁读取来确保我看到的是当前值,因为毕竟该字段不是可变的。 我一直在使用Interlocked.CompareExchange(int, 0, 0)来实现。
但是,我偶然发现了这个答案,这表明实际上普通读取总是会看到Interlocked.Increment ed值的当前版本,并且由于int读取已经是原子的,因此无需执行任何特殊操作。 我还发现了一个请求,其中Microsoft拒绝了对Interlocked.Read(ref int)的请求,进一步表明这是完全多余的。
所以我真的可以安全地读取没有Interlocked的int字段的最新值吗?
-
想要选择答案吗? :)
-
@sunside不,我不认为我可以自信地选择答案,因为我从未相信过任何一种方法。 无论如何,选择最高投票的答案也没有意义。 充其量我的勾号会欺骗用户以为我知道答案是正确的。
-
足够公平和理解。 只是想确保它没有被遗忘。
如果要保证另一个线程将读取最新值,则必须使用Thread.VolatileRead()。 (*)
读取操作本身是原子性的,因此不会造成任何问题,但是如果没有volatile读取,您可能会从缓存中获取旧值,或者编译器可能会优化您的代码并完全消除读取操作。从编译器的角度来看,代码在单线程环境中工作就足够了。易失性操作和内存障碍用于限制编译器优化和重新排序代码的能力。
有几个参与者可以更改代码:编译器,JIT编译器和CPU。哪一个表明您的代码已损坏并不重要。唯一重要的是.NET内存模型,因为它指定了所有参与者必须遵守的规则。
(*)Thread.VolatileRead()并没有真正获得最新值。它将读取该值,并在读取后添加一个内存屏障。第一个易失性读取可能会获取缓存的值,而第二个易失性读取可能会获取更新的值,因为如果需要的话,第一个易失性读取的内存屏障已强制进行缓存更新。实际上,编写代码时,此细节并不重要。
-
+1,这取决于他关心原子读取的"为什么"。 (还添加了MSDN链接)
-
我必须?例如,如果我使用Interlocked.Read(长时间)或Interlocked.CompareExchange(整数),那么我不一定会获得最新值?
-
不,他的意思是与x = y这样的裸读相对。
-
@romkyns:Interlocked方法是安全的,并且它们始终以新值运行。但是,如果只是在其他位置读取变量值,则获取的值不一定与使用Interlocked方法更新的值相同。即使Interlocked方法已将该值写入主内存,您的线程也可能在另一个未更新其读取缓存的内核中执行,并且您将获得旧值。如果您将变量用作循环的退出条件,或者期望指令具有特定的执行顺序,那么这确实是一个问题。
-
@mgronber如果是这样,那么我链接的答案是错误的。同样,被拒绝的MS Connect建议实际上并不是多余的。你同意吗?
-
我不同意您对MS Connect链接的评估。在32位内核上需要Interlocked.Read(Int64),以确保您不会获得Int64的一半。只有32位读取是原子的。它与获取最新值无关。您正在混淆同步和原子性。
-
@romkyns:是的,如果我们考虑.NET内存模型,我认为答案是错误的。如果没有内存障碍,则可以优化常规读取操作,因此Interlocked更新不足。必须使用易失性读取或将变量指定为易失性。
-
@romkyns:就像sixlettervariables所说的那样,存在Interlocked方法来提供原子性,因此MS Connect建议被正确拒绝了。同步只是某些Interlocked方法的副作用。
-
@mgronber那么Interlocked.Read(Int64)是否执行易失性读取?
-
@binki:I.12.6.5锁和线程:" 5.显式原子操作。类库在System.Threading.Interlocked类中提供了各种原子操作。这些操作(例如,Increment,Decremenet,Exchange和CompareExchange)执行隐式获取/释放操作。" Ecma规范未明确说明Interlocked.Read(Int64)。我假设它在64位系统上可以正常读取,因为Interlocked类的目的是提供原子性,而64位读取在64位系统上已经是原子的。所以我的回答是不。
有点元问题,但是关于使用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(原子性,一致性,隔离性和持久性)可能会帮助您使用术语。
-
元注释:如果您的问题是应该使用哪种同步方法,则@mgronber可以解决此问题。
-
如果在同一方法中(在下一行中)Interlocked.Increment(a)之后立即读取变量int a,是否应该使用任何同步?
是的,您阅读的所有内容都是正确的。 Interlocked.Increment的目的是使对字段进行更改时正常读取不会为假。读字段不是危险的,写字段是危险的。