写入/读取何时影响主存储器?

时间:2009-11-14 13:12:42

标签: c# multithreading lock-free memory-model memory-fences

当我在一个字段中写入一个值时,在将新值保存在主内存中时,我能得到什么保证?例如,我怎么知道处理器没有将新值保留在它的私有缓存中,而是更新了主内存?
另一个例子:

int m_foo;

void Read() // executed by thread X (on processor #0)
{
   Console.Write(m_foo);
}

void Write() // executed by thread Y (on processor #1)
{
   m_foo = 1;
}

有可能在 Write()完成执行后,其他一些线程执行 Read()但实际上会看到“0”作为当前值吗? (因为以前对m_foo的写入可能还没有刷新?) 什么样的原语(除了锁)可用于确保写入被刷新?


修改
在我使用的代码示例中,写入和读取以不同的方法放置。 Thread.MemoryBarrier不会影响同一范围内存在的指令重写吗?

另外,假设它们不会被JIT内联,我怎样才能确保写入m_foo的值不会存储在寄存器中,而是存储在主存中? (或者,当读取m_foo时,它不会从CPU缓存中获取旧值)。

是否可以在不使用锁或'volatile'关键字的情况下实现此目的? (另外,假设我没有使用原始类型,但是WORD大小的结构 [挥发性无法应用] 。)

4 个答案:

答案 0 :(得分:11)

如果您想确保及时和有序地书写,请将其标记为volatile,或者(更多的痛苦)使用Thread.VolatileRead / Thread.VolatileWrite(不是一个有吸引力的选项) ,很容易错过一个,使它无用)。

volatile int m_foo;

否则你几乎没有任何保证(只要你说多个线程)。

您可能还想查看锁定(Monitor)或Interlocked,只要 相同就可以达到相同的效果方法用于所有访问(即所有lock或全部Interlocked等)。

答案 1 :(得分:3)

已经提到过Volatile和Interlocked,你要求提供原语,列表的一个补充就是在写或读之前使用Thread.MemoryBarrier()。这保证不对存储器写入和读取进行重新排序。

这是“手动”,lockInterlockedvolatile可以在大多数情况下自动执行。您可以使用它作为任何其他技术的完全替代品,但它可以说是最难的旅行路径,所以MSDN说:

  

“通过使用很难构建正确的多线程程序   内存屏障。对于大多数目的而言   C#lock语句,Visual Basic   SyncLock语句和方法   Monitor类提供了更简单的方法   减少错误的同步方式   内存访问。我们建议你   使用它们而不是MemoryBarrier。 “

如何使用MemoryBarrier

一个很好的例子是VolatileReadVolatileWrite的实现,它们都在内部使用MemoryBarrier。要遵循的基本经验法则是:当您读取变量时,在读取之后放置一个内存屏障。在写入值时,内存障碍必须在写入之前

如果您怀疑这是否效率低于lock,请考虑锁定只不过是“完全防护”,因为它在代码块之前和之后放置了一个内存屏障(忽略Monitor for片刻)。这个原则在excellent definitive article on threads, locking, volatile and memory barriers by Albahari中得到了很好的解释。

来自反射器:

public static void VolatileWrite(ref byte address, byte value)
{
    MemoryBarrier();
    address = value;
}

public static byte VolatileRead(ref byte address)
{
    byte num = address;
    MemoryBarrier();
    return num;
}

答案 2 :(得分:2)

只要您不使用任何同步,就无法保证在一个处理器上运行的线程看到另一个处理器上运行的另一个线程所做的更改。那是因为该值可以缓存在CPU缓存中或CPU寄存器中。

因此,您需要将变量标记为volatile。这将在读取写入之间创建“发生前 - 先发生”。

答案 3 :(得分:2)

这不是处理器缓存问题。写入通常是传递(写入到缓存和主存储器),所有读取都将访问缓存。但是在路上还有许多其他缓存(编程语言,库,操作系统,I/O缓冲区等)。编译器还可以选择将变量保存在处理器寄存器中,并且永远不要将其写入主存储器(这就是易失性运算符的设计目的,避免在存储器映射I / O时将值存储在寄存器中)。 p>

如果您有多个进程或多个线程并且同步是一个问题,您必须明确地进行,有很多方法可以根据用例进行。

对于单线程程序,不关心,编译器将执行必要的操作,读取将访问已编写的内容。