【问题标题】:When do writes/reads affect main memory?写入/读取何时影响主内存?
【发布时间】:2009-11-14 13:12:41
【问题描述】:

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

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 大小的结构 [因此无法应用 volatile]。)

【问题讨论】:

  • 如果您有 WORD 大小的结构,您可以将它们转换为 uint 并且您可以应用 volatile 或各种答案中讨论的任何其他技术。

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


【解决方案1】:

如果你想确保它写得及时和有序,那么把它标记为volatile,或者(更痛苦)使用Thread.VolatileRead/Thread.VolatileWrite(不是一个有吸引力的选择,而且很容易错过一个,使其无用)。

volatile int m_foo;

否则,您几乎无法保证任何事情(只要您谈论多个线程)。

您可能还想查看锁定 (Monitor) 或 Interlocked,这将达到相同的效果只要使用 same 方法所有访问权限(即所有lock,或所有Interlocked等)。

【讨论】:

  • Volatile 甚至不能真正保证“及时”位 - 只是“有序”。我决定我没有正确理解易失性......整个内存模型太混乱了。我只会在使用 PFX 等其他框架时进行无锁编码...
  • 关于volatile 的注释:当读取跟随写入时,不能保证这个“顺序”,因此使volatile 不像人们所期望的那样完美。这似乎是一个鲜为人知的事实,但却是灾难的保证。除其他外,这在此处进行了解释:albahari.com/threading/part4.aspx#_Memory_Barriers
  • 也就是说,如果需要read-after-write,则必须使用full fence,并使用Interlocked(使用full fences)或传统锁定。
  • @Abel:我只是指写的顺序。
  • “易失性”(和一般的内存栅栏)不会只影响指令顺序,还是它也会影响值从寄存器“刷新”到主存储器的时间? (或者,强制 CPU 从主内存而不是私有缓存中获取值)。是否可以告诉处理器在不使用 'volatile' 关键字的情况下从主存储器中读取值? (假设我有一个 4 字节的结构,而不是原始类型。然后呢?
【解决方案2】:

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

这是“手动”完成lockInterlockedvolatile 大部分时间可以自动完成的操作。您可以将其用作任何其他技术的完全替代品,但它可以说是最困难的旅行路径,MSDN 是这样说的:

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

如何使用内存屏障

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

如果您怀疑这是否比lock 效率低,请考虑锁定只不过是“完全围栏”,因为它在代码块之前和之后放置了一个内存屏障(暂时忽略 Monitor )。这个原理在这个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;
}

【讨论】:

  • 在我使用的代码示例中,写入和读取被放置在不同的方法中。 Thread.MemoryBarrier 不只影响存在于同一范围内的指令重新排序吗?另外,假设它们不会被 JIT 内联,我如何确保写入 m_foo 的值不会存储在寄存器中,而是存储到主存储器中? (或者当 m_foo 被读取时,它不会从 CPU 缓存中获取旧值)。是否可以在不使用锁或 'volatile' 关键字的情况下实现这一目标?
  • 是的,是的,您可以为此使用 MemoryBarrier。但是,这是否明智是另一回事。我将通过对这一点的解释来更新我的问题。
  • 那么实际上,如果我在写入新值后使用栅栏,就可以保证新值会从寄存器移动到主存中?如果我在读取值之前使用栅栏,那么它实际上是在使缓存无效,使 CPU 也从主内存中获取值?
  • 是的。我不知道它是否真的使 CPU 缓存无效,但可以保证当您从该地址读取时(如果读取放在 MemoryBarrier 之后),写入该地址的任何指令都将完成。这是一个很好的附加读物,用更一般的术语解释了这个原则:en.wikipedia.org/wiki/Memory_barrier
【解决方案3】:

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

因此,您需要将变量标记为 volatile。这将在读取和写入之间创建一个“发生前”的关系。

【讨论】:

    【解决方案4】:

    这不是处理器缓存问题。写入通常是直通的(写入同时进入缓存和主内存),所有读取都将访问缓存。但是还有很多其他的缓存(编程语言、库、操作系统、I/O 缓冲区等)。编译器还可以选择将变量保留在处理器寄存器中,并且从不将其写入主存储器(这就是 volatile 运算符的设计目的,当它可以是内存映射 I/O 时避免将值存储在寄存器中)。

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

    对于单线程程序,不要在意,编译器会做它必须做的事情,读取会访问已写入的内容。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-10-09
      • 2010-10-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-24
      相关资源
      最近更新 更多