使用Interlocked.Exchange更新引用和Int32类型

30

众所周知,32位处理器中一个引用占用4字节内存,而64位处理器中占用8字节。因此,处理器保证以机器自然字大小的增量进行的单个内存读取和写入将被原子地执行。

另一方面,Interlocked类中有两种方法:

public static int Exchange(
    ref int location1,
    int value
)

并且

public static T Exchange<T>(
    ref T location1,
    T value
)
where T : class

因此,问题是为什么对于Int32和引用类型需要使用Interlocked.Exchange?难道不能通过简单赋值来安全地完成,因为它是原子性的吗?
4个回答

26

这不仅仅涉及原子性问题,还涉及内存可见性。变量可以存储在主内存中或CPU缓存中。如果该变量只存储在CPU缓存中,则在运行在不同CPU上的线程中将无法看到该变量。考虑下面的例子:

public class Test {
    private Int32 i = 5;

    public void ChangeUsingAssignment() {
        i = 10;
    }

    public void ChangeUsingInterlocked() {
        Interlocked.Exchange(ref i, 10);
    }

    public Int32 Read() {
        return Interlocked.CompareExchange(ref i, 0, 0);
    }
}

如果在一个线程上调用“ChangeUsingAssignment”,而在另一个线程上调用“Read”,返回值可能是5,而不是10。但是如果调用ChangeUsingInterlocked,“Read”将按预期返回10。

 ----------         ------------         -------------------
|   CPU 1  |  -->  |   CACHE 1  |  -->  |                   |
 ----------         ------------        |                   |
                                        |        RAM        |
 ----------         ------------        |                   |
|   CPU 2  |  -->  |   CACHE 2  |  -->  |                   |
 ----------         ------------         -------------------
在上面的图表中,“ChangeUsingAssignement”方法可能导致值10被“卡住”在CACHE 2中,无法到达RAM。当CPU 1稍后尝试读取它时,它将从RAM中获取仍为5的值。使用Interlocked而不是普通写入将确保值10完全到达RAM。

4
我知道这已经是一年后了,但如果可能的话,能否请您审核一下?这个网站http://igoro.com/archive/volatile-keyword-in-c-memory-model-explained/似乎暗示着C#中的所有写操作已经是volatile的。 - user981225
4
你对于内存可见性的看法是正确的,但是你的解释并不准确。一旦数值进入处理器缓存中,它就对所有物理CPU核心可见。这是由https://en.wikipedia.org/wiki/Cache_coherence所保证的。内存可见性的问题在于数值仍然存储在CPU核心内的读写缓冲区中(这不是CPU的L1、L2、L3缓存)。 - ekalchev

10
< p > < code > Interlocked.Exchange 具有返回值,使您能够知道刚刚替换的值。这些方法实现了设置新值和获取旧值的组合。 < / p >

所以,如果您不需要旧值,您可以简单地赋值,但是那么您也需要一个屏障调用来使缓存失效。 - H H
@Henk:你能否请提供更多详细信息,您所说的Barrier call是指什么? - Oleg Dudnyk
@Henk:很抱歉,但在这里没有关于Barrier调用的任何内容:http://www.google.com.ua/search?q=Barrier+call+site%3Ahttp%3A%2F%2Fwww.albahari.com%2Fthreading%2F&hl=uk&num=10&lr=&ft=i&cr=&safe=images 你能给一些例子吗? - Oleg Dudnyk
@gorik:这是第四部分,全名是MemoryBarrier。但不要走那条路。 - H H

6

一般情况下,交换内存值和CPU寄存器内容不是原子操作。您需要读取和写入内存位置。此外,Interlocked方法保证即使在具有自己的缓存和可能拥有主存储器不同视图的多核计算机上,该操作也是原子的。


但是如果我不需要旧值,只想将新值分配给某个变量。这只是写入和原子操作,对吗? - Oleg Dudnyk
@OlegDudnyk 我认为这不正确,即使您不关心原始值。考虑如何在汇编语言中实现,取决于您的src和dest变量/值在哪里,它们可能被转换为多个指令,在2个指令之间,您的应用程序仍然可能会被中断。在某些体系结构上,您可以锁定总线以确保没有人可以读写内存(例如,IA具有LOCK前缀),并且某些指令保证是原子的(例如,IA上的XCHG,CMPXCHG等)。 - codewarrior

6

Interlock.Exchange返回原始值,同时执行原子操作。其主要目的是提供锁定机制。因此实际上需要两个操作:读取原始值和设置新值。这两个操作一起并不是原子操作。


1
但如果我不需要旧值,只想将新值分配给某个变量。这只是写入和原子的,对吗? - Oleg Dudnyk
@gorik:如果是对任何引用类型的变量或占用四个字节或更少(32位机器)的任何内置值类型进行读取或写入操作,则是保证原子性的。查看Eric Lippert在此主题上的一系列帖子,它们会有所帮助:原子性、易失性和不可变性是不同的,第一部分 - InBetween

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接