我对以下代码示例有疑问(m_值 不是易失性的,每个线程都在单独的处理器上运行)

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;
}

是否使用 联锁交换 in Foo() 保证当执行 Bar() 时,我会看到值“1”?(即使该值已经存在于寄存器或缓存行中?)或者我是否需要在读取值之前放置内存屏障 m_值?

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

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

有帮助吗?

解决方案

内存屏障使用通常的模式匹配,你会放一个关键部分的执行什么,但是分成了对生产者和消费者。作为一个例子爆部执行将通常是以下形式:

while (!pShared->lock.testAndSet_Acquire()) ;
// (this loop should include all the normal critical section stuff like
// spin, waste, 
// pause() instructions, and last-resort-give-up-and-blocking on a resource 
// until the lock is made available.)

// Access to shared memory.

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

pShared->lock.clear_Release()

以上采集存储器屏障可确保可在成功锁定修改前已经被启动的任何负载(pShared->咕)被抛出,如果neccessary重新启动。

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

您必须在典型的生产国和消费原子标志之情况类似的模式(这是很难通过你的样品来告诉如果这是你在做什么,但应该说明的想法)。

假设你的生产者使用的原子变量以指示某些其他状态是准备好使用。你会想是这样的:

pShared->goo = 14

pShared->atomic.setBit_Release()

如果没有“写”屏障这里的生产者,你有没有保证,硬件不通过存储层次结构中要得到的原子店咕商店已经通过CPU存储队列发前,和最多它是可见的(即使你有,以确保编译器订单的事情你所希望的方式的机制)。

在消费者

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

在这里没有“读”屏障你不会知道,硬件一直没有去和原子访问之前,你取出咕完成。的原子(即:存储器与所述互锁函数做的东西等锁CMPXCHG操作),仅是“原子”相对于本身,而不是其他存储器

现在,必须被提到的其余的是,所述阻挡结构是高度不可移植。你可能编译器提供_acquire和_release的变化对于大多数的原子操作方法,而这些都是,你会使用它们的方式排序。根据您所使用的平台(即:IA32),这很可能正是你会得到什么,而不_acquire()或_release()后缀。平台哪里该事项IA64和PowerPC(除了HP的地方仍有小幅有效地抽搐死亡)。 IA64有.acq和最上的加载和存储指令(包括原子的像CMPXCHG)的.rel指令改性剂。的PowerPC具有用于此单独的指令(的iSync和lwsync给你的读取和写入分别屏障)。

现在。说了这么多。你真的有一个很好的理由走这条路?做这一切正确是非常困难的。对于在代码审查很多自我怀疑和不安全感的准备,并确保你有很多高并发测试的用各种随机时间scenerios的。除非你有一个非常非常好的理由,以避免它,不要自己编写关键部分使用的关键部分。

其他提示

记忆障碍并不特别帮你。他们指定内存操作之间的顺序,在这种情况下,每个线程只有一个内存操作,也不要紧。一个典型的场景被写入非原子到字段中的结构,一个内存屏障,则结构的地址发布给其他线程。该壁垒保证了写入到结构成员由所有CPU看到他们得到它的地址之前。

你真正需要的是一个基本操作,即。 InterlockedXXX功能,或在C#volatile变量。如果在酒吧之间读取的原子,你可以保证,无论是编译器,也不是CPU,是否阻止其阅读无论是在富写之前,或之后在其最先被执行Foo中写入取决于价值的任何优化。既然你说你“知道”之前,酒吧的读Foo的写入操作,然后将酒吧总是返回true。

如果没有在酒吧读是原子的,它可以被读取部分更新值(即垃圾),或高速缓存的值这两者(无论是从编译器或从CPU),可以防止酒吧从返回true,其它应该。

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

我不能完全肯定,但我认为Interlocked.Exchange将使用的该反正提供了全存储器屏障窗户API 的InterlockedExchange功能。

  

此功能产生一个完整的存储器   屏障(或篱笆),以确保   内存操作中完成   顺序。

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

以下同步函数使用适当的屏障 要确保内存排序,请执行以下操作:

  • 进入或离开临界区的函数

  • 向同步对象发出信号的函数

  • 等待功能

  • 联锁功能

(来源 : 关联)

但你对寄存器变量不走运。如果 m_value 位于 Bar 的寄存器中,您将看不到 m_value 的更改。因此,您应该将共享变量声明为“易失性”。

如果m_value没有被标记为volatile,则没有理由认为在Bar读出的值是围栏。编译器的优化,缓存,或其他因素可能重新排序的读取和写入。当它在适当的围栏内存引用的生态系统是用来交换互锁仅是有帮助的。这标志着场volatile整点。在.NET的内存模型是不是直线前进因为有些人可能想到。

Interlocked.Exchange()应保证值刷新到正确的所有CPU。 - 它提供了它自己的存储器屏障

我很惊讶,编译器complaing关于通过挥发进入Interlocked.Exchange() - 您使用Interlocked.Exchange()应该几乎强制volatile变量的事实。

您的问题的可能看到的是,如果编译器栏()的一些沉重的优化和认识到没有什么变化m_value它可以优化掉你的支票的价值。这就是volatile关键字会做 - 这将暗示该变量可以优化的视图之外改变编译器

如果你不告诉编译器或运行时m_value不应该提前阅读栏(),它可以和可能提前m_value的缓存Bar()的价值,简单地使用缓存的值。如果你想确保它看到m_value的“最新”版本,无论是在Thread.MemoryBarrier()或使用Thread.VolatileRead(ref m_value)推。后者是比全存储器屏障更便宜。

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

编辑:去想它的另一种方式是,真的有两种内存障碍:即告诉编译器如何序列读取和写入,并且告诉CPU如何序列读取和写入CPU内存屏障编译器内存屏障。该Interlocked函数使用CPU内存屏障。即使编译它们当作编译器内存屏障,它仍然不会有问题,因为在这种特殊情况下,Bar()可能已被单独编译,而不是m_value的其他用途,要求编译器内存屏障而闻名。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top