互锁和记忆障碍

我有一个关于以下代码示例的问题( m_value不是volatile,每个线程都在一个单独的处理器上运行)

void Foo() // executed by thread #1, BEFORE Bar() is executed { Interlocked.Exchange(ref m_value, 1); } bool Bar() // executed by thread #2, AFTER Foo() is executed { return m_value == 1; } 

在Foo()中使用Interlocked.Exchange是否保证在执行Bar()时,我会看到值“1”? (即使该值已经存在于寄存器或缓存行中?)或者在读取m_value的值之前是否需要设置内存屏障?

另外(与原始问题无关),声明一个volatile成员并通过引用InterlockedXX方法传递它是合法的吗? (编译器警告通过引用传递volatile,所以在这种情况下我应该忽略警告吗?)

请注意 ,我不是在寻找“更好的做事方式”,所以请不要发布建议完全替代方式(“使用锁定”等)的答案,这个问题来自于纯粹的兴趣..

内存屏障使用的常用模式与您在临界区的实现中所使用的模式匹配,但是为生产者和使用者分成对。 作为示例,您的关键部分实现通常具有以下forms:

 while(!pShared-> lock.testAndSet_Acquire());
 //(这个循环应该包括所有正常的关键部分
 //旋转,浪费, 
 //暂停()指令,以及资源上的最后一次放弃和阻止 
 //直到锁定可用。)

 //访问共享内存。

 pShared-> foo = 1 
 v = pShared-> goo

 pShared-> lock.clear_Release()

获取上面的内存屏障可确保在成功锁定修改之前可能已启动的任何加载(pShared-> goo)被抛出,以便在必要时重新启动。

释放存储器屏障确保在保护共享存储器的锁定字被清除之前,从goo到(本地说)变量v的负载完成。

在典型的生产者和消费者primefaces标志场景中你有一个类似的模式(你的样本很难说这是你在做什么,但应该说明这个想法)。

假设您的生产者使用primefaces变量来指示某些其他状态已准备好使用。 你会想要这样的东西:

 pShared-> goo = 14

 pShared-> atomic.setBit_Release()

如果生成器中没有“写入”障碍,则无法保证硬件在goo存储通过cpu存储队列之前不会到达primefaces库,而是通过内存层次结构可以看到它(即使你有一种机制可以确保编译器以你想要的方式命令)。

在消费者中

 if(pShared-> atomic.compareAndSwap_Acquire(1,1))
 {
    v = pShared-> goo 
 }

如果没有“读取”障碍,您将无法知道在primefaces访问完成之前硬件没有消失并为您提取goo。 primefaces(即:使用互锁函数操作的内存执行像锁cmpxchg这样的东西),相对于自身而言只是“primefaces”,而不是其他内存。

现在,必须提到的其余事情是屏障构造非常不可移植。 您的编译器可能为大多数primefaces操作方法提供了_acquire和_release变体,这些是您使用它们的各种方法。 根据您使用的平台(即:ia32),如果没有_acquire()或_release()后缀,这些可能就是您所能得到的。 这个问题很重要的平台是ia64(除了惠普,它仍然会轻微抽搐,但实际上已经死了)和powerpc。 ia64在大多数加载和存储指令(包括像cmpxchg这样的primefaces指令)上都有.acq和.rel指令修饰符。 powerpc有针对此的单独指令(isync和lwsync分别为您提供读写障碍)。

现在。 说完了这一切。 你真的有充分的理由走这条路吗? 正确地做这一切可能非常困难。 为代码审查中的许多自我怀疑和不安全做好准备,并确保您有各种随机时序场景的高并发性测试。 使用临界区除非你有一个非常好的理由要避免它,并且不要自己写那个关键部分。

记忆障碍对您没有特别的帮助。 它们指定了内存操作之间的顺序,在这种情况下,每个线程只有一个内存操作,因此无关紧要。 一种典型的情况是非primefaces地写入结构中的字段,内存屏障,然后将结构的地址发布到其他线程。 Barrier保证在获取结构成员地址之前,所有CPU都可以看到对结构成员的写入。

你真正需要的是primefaces操作,即。 InterlockedXXX函数或C#中的volatile变量。 如果Bar中的读取是primefaces的,那么你可以保证编译器和cpu都不会做任何阻止它在Foo中写入之前读取值的优化,或者在Foo中写入后根据首先执行的执行。 因为你说你“知道”Foo的写作发生在Bar的阅读之前,所以Bar总是会返回true。

如果没有读取Bar是primefaces的,它可能是读取部分更新的值(即垃圾),或缓存的值(来自编译器或来自CPU),这两者都可能阻止Bar返回true应该是什么。

大多数现代CPU保证字对齐读取是primefaces的,所以真正的诀窍是你必须告诉编译器读取是primefaces的。

我不完全确定,但我认为Interlocked.Exchange将使用windows API的InterlockedExchange函数,无论如何都会提供完整的内存屏障。

此函数生成完整的内存屏障(或栅栏),以确保按顺序完成内存操作。

互锁交换操作保证了内存屏障。

以下同步函数使用适当的障碍来确保内存排序:

  • 进入或离开关键部分的function

  • 信号同步对象的函数

  • 等待function

  • 互锁function

(来源: 链接 )

但你对寄存器变量不满意。 如果m_value位于Bar中的寄存器中,则不会看到对m_value的更改。 因此,您应该声明共享变量’volatile’。

如果m_value未标记为volatile ,则没有理由认为Bar读取的值是隔离的。 编译器优化,缓存或其他因素可能会重新排序读取和写入。 互锁交换仅在用于适当围栏内存引用的生态系统中时才有用。 这是标记场volatile 。 .Net内存模型并不像某些人预期的那样直截了当。

Interlocked.Exchange()应该保证将值正确地刷新到所有CPU – 它提供了自己的内存屏障。

令我感到惊讶的是编译器抱怨将volatile传递给Interlocked.Exchange() – 你使用Interlocked.Exchange()的事实几乎应该强制要求一个volatile变量。

可能会看到的问题是,如果编译器对Bar()进行了一些重要的优化,并且意识到没有任何内容会改变m_value的值,那么它可以优化您的检查。 这就是volatile关键字的作用 – 它会暗示编译器可以在优化器视图之外更改该变量。

如果你没有告诉编译器或运行时不应该在Bar()之前读取m_value ,它可以并且可以在Bar()之前缓存m_value的值,并且只使用缓存的值。 如果要确保它看到m_value的“最新”版本,请m_value Thread.MemoryBarrier()或使用Thread.VolatileRead(ref m_value) 。 后者比全内存屏障便宜。

理想情况下,你可以推入ReadBarrier,但CLR似乎并不直接支持它。

编辑:另一种思考方式是存在两种内存障碍:编译器内存屏障告诉编译器如何对读取和写入进行排序,以及CPU内存屏障告诉CPU如何对读取和写入进行排序。 Interlockedfunction使用CPU内存屏障。 即使编译器将它们视为编译器内存屏障,它仍然无关紧要,因为在这种特定情况下, Bar()可能已被单独编译,并且不知道需要编译器内存屏障的m_value的其他用法。