Interlocked and volatile
Interlocked and volatile
我有一个变量,用于表示状态。它可以从多个线程读取和写入。
我使用`Interlocked.Exchange`和`Interlocked.CompareExchange`来更改它。但是我从多个线程中读取它。
我知道`volatile`可以用来确保变量不会被本地缓存,而是直接从内存中读取。
然而,如果我将变量设置为`volatile`,那么会生成一个关于使用`volatile`并使用`ref`传递给`Interlocked`方法的警告。
我希望确保每个线程都读取变量的最新值,而不是一些缓存版本,但是我不能使用`volatile`。
有一个`Interlocked.Read`,但它是针对64位类型的,并且在紧凑框架中不可用。它的文档说,对于32位类型来说,它是不需要的,因为它们已经在单个操作中执行了。
互联网上有一些说法,如果你在所有访问中都使用Interlocked方法,就不需要使用volatile。然而,你无法使用Interlocked方法读取32位变量,所以你无法在所有访问中使用Interlocked方法。
有没有一种方法可以在不使用锁的情况下实现变量的线程安全读写?
Interlocked和volatile操作通常不应该同时使用。得到警告的原因是因为它(几乎?)总是表示您对正在进行的操作有误解。
简单来说,volatile表示每次读取操作都需要重新从内存中读取,因为可能有其他线程更新了变量。当应用于可以由您运行的架构以原子方式读取/写入的字段时,这应该是您所需做的,除非您使用long/ulong,大多数其他类型都可以被以原子方式读取/写入。
当字段没有标记为volatile时,您可以使用Interlocked操作来提供类似的保证,因为它会刷新缓存,以便更新对所有其他处理器可见...这样做的好处是将开销放在更新上而不是读取上。
这两种方法中哪种执行最佳取决于您正在做的具体内容。这种解释过于简单化了。但是从这一点上应该清楚,同时使用这两种方法是毫无意义的。
好的,所以如果我只使用Interlocked进行写操作,那么普通读取将始终是最新的吗?
一般来说,应该可以胜任,是的...这就是为什么(除了long/ulong之外)没有仅用于读取值的Interlocked操作。
同时使用volatile和Interlocked没有任何固有问题。得到警告的原因是volatile不是类型系统的一部分,而收到引用的被调用方将不知道所引用的变量需要volatile访问。
是的,我的第一句话可能可以更清楚一些...警告不是因为混合使用'Interlocked'和'volatile',而是因为混合使用'ref'和'volatile'。但是这并不改变警告指示对正在进行的操作的误解这一事实。
就记录而言,在这里有讨论,其中建议此答案是错误的。如果您希望做出贡献,那将非常好。
当我第一次写这个答案时,我意识到其中有更多微妙之处。我强烈建议阅读Joe Duffy(和/或他关于并发性的书)以在所讨论的问题上获得详细的理解。他在这方面确实是权威人物。无锁的一个重大问题通常与某种程度的性能有关,但在这一过程中存在可以产生不正确结果或降低所寻求的性能的陷阱。
Joe Duffy在这篇博客文章中具体讨论了对volatile/Interlocked操作的引用。混合使用Interlocked和引用是100%可以的,他已经向C#编译器申请特殊情况,不会在这种情况下抛出警告。
感谢您找到这个参考...当我参考Joe Duffy的网站时,它已经关闭了,我只模糊地记得他写了什么。
那么主要问题是编译器(或JIT)可能会优化掉仅读取值的线程的直接内存访问,除非您将变量标记为volatile,对吗?
- 晚回复,但是...我认为问题更多与一个处理器核心可能会缓存内存读取,因此它不会在后续读取中看到另一个核心执行的更新有关。
Joe Duffy在其新博客主页中详细讨论了volatile和Interlocked操作:joeduffyblog.com/2008/06/13/..., joeduffyblog.com/2009/02/02/..., joeduffyblog.com/2007/11/10/clr-20-memory-model
假设x是一个共享变量。在一个线程中,我使用a+=x; a+=x;,其中a是线程的本地变量。在第二个线程中,我使用Interlocked.Increment(ref x);。如果没有volatile,第一个线程可能会将x放入寄存器,并将其用于其两个语句,从而错过增量。所以即使使用Interlocked,volatile似乎也是必需的。
在使用Interlocked和volatile时,有以下问题和解决方法:
问题:
- 当使用Interlocked.Xxx函数时,可以安全地忽略编译器的警告,因为这些函数总是执行volatile操作。因此,对于共享状态,使用volatile变量是完全可以的。
- 如果想要尽可能地消除警告,可以使用Interlocked.CompareExchange(ref counter, 0, 0)进行Interlocked读取。
解决方法:
- 对于直接写入状态变量(即不使用Interlocked.Xxx)的情况,需要在状态变量上使用volatile修饰符。
- 使用Interlocked操作(或volatile操作)更新的变量的读取将使用最近的值。
示例:
- 假设x是一个共享变量。在一个线程中,执行a += x; a += x;其中a是线程本地变量。在第二个线程中,执行Interlocked.Increment(ref x)。如果没有使用volatile修饰符,第一个线程可能会将x存储在寄存器中,并在两个语句中都使用相同的x值,从而错过了增量操作。因此,即使使用Interlocked,volatile仍然是必需的。
通过使用Interlocked和volatile,可以安全地处理共享状态和多线程操作。Interlocked函数提供了原子操作,而volatile修饰符则确保了变量的可见性和顺序性。这样可以避免编译器或JIT优化导致的问题。在具体的应用中,根据需要选择合适的方法来处理共享状态和多线程操作。