multithreading - 我如何理解读取内存障碍和 volatile

标签 multithreading volatile memory-barriers

某些语言提供 volatile被描述为在读取支持变量的内存之前执行“读取内存屏障”的修饰符。

读取内存屏障通常被描述为一种确保 CPU 在执行屏障之前请求的读取之前执行屏障之后请求的读取的方法。但是,使用此定义,似乎仍然可以读取过时的值。换句话说,以某种顺序执行读取似乎并不意味着必须咨询主存或其他 CPU 以确保后续读取的值实际上反射(reflect)了读取屏障时系统中的最新值,或者在读取屏障之后随后写入的值。阅读障碍。

那么, volatile 真的保证读取的是最新值,还是只是(喘气!)读取的值至少与屏障之前的读取一样最新?或者其他的解释?这个答案的实际意义是什么?

最佳答案

有读障碍和写障碍;获得障碍和释放障碍。还有更多(io vs 内存等)。

控制“最新”值或“新鲜度”值的障碍并不存在。它们用于控制内存访问的相对顺序。

写屏障控制写的顺序。因为写入内存很慢(与 CPU 的速度相比),通常有一个写入请求队列,写入在“真正发生”之前发布。尽管它们按顺序排队,但在队列内部时,写入可能会重新排序。 (所以也许“队列”不是最好的名字......)除非你使用写屏障来防止重新排序。

读屏障控制读的顺序。由于推测性执行(CPU 提前查看并提前从内存加载)以及写入缓冲区的存在(如果存在,CPU 将从写入缓冲区而不是内存中读取值 - 即 CPU 认为它只是写了 X = 5,那为什么要读回来,只是看写缓冲区还在等待变成5)读取可能会发生乱序。

无论编译器试图对生成的代码的顺序做什么,这都是正确的。即 C++ 中的“ volatile ”在这里无济于事,因为它只告诉编译器输出代码以重新读取“内存”中的值,它不会告诉 CPU 如何/从哪里读取它(即“内存”在 CPU 级别有很多事情)。

所以读/写屏障设置了块以防止在读/写队列中重新排序(读取通常不是一个队列,但重新排序的效果是相同的)。

有哪些类型的块? - 获取和/或释放块。

Acquire - 例如 read-acquire(x) 会将 x 的读取添加到读取队列中并刷新队列(不是真正刷新队列,而是添加一个标记,说在此读取之前不要重新排序任何内容,就好像队列被刷新)。所以稍后(按代码顺序)读取可以重新排序,但不能在读取 x 之前重新排序。

发布 - 例如 write-release(x, 5) 将首先刷新(或标记)队列,然后将写入请求添加到写入队列。因此,较早的写入不会在 x = 5 之后重新排序,但请注意,稍后的写入可以在 x = 5 之前重新排序。

请注意,我将读取与获取和写入与释放配对,因为这是典型的,但不同的组合也是可能的。

获取和释放被认为是“半壁垒”或“半栅栏”,因为它们只会阻止重新排序以一种方式进行。

完整屏障(或完整栅栏)适用于获取和释放 - 即没有重新排序。

通常对于无锁编程,或 C# 或 java 'volatile',您想要/需要的是
读-获取和写-释放。

IE

void threadA()
{
   foo->x = 10;
   foo->y = 11;
   foo->z = 12;
   write_release(foo->ready, true);
   bar = 13;
}
void threadB()
{
   w = some_global;
   ready = read_acquire(foo->ready);
   if (ready)
   {
      q = w * foo->x * foo->y * foo->z;
   }
   else
       calculate_pi();
}

所以,首先,这是一种糟糕的线程编程方式。锁会更安全。但只是为了说明障碍......

threadA() 写完 foo 后,需要写 foo->ready LAST,真的是最后,否则其他线程可能会提前看到 foo->ready 并得到错误的 x/y/z 值。所以我们使用 write_release在 foo->ready 上,如上所述,它有效地“刷新”了写入队列(确保 x,y,z 已提交),然后将 ready=true 请求添加到队列中。然后添加 bar=13 请求。请注意,由于我们刚刚使用了一个释放屏障(不是完整的) bar=13 可能会在准备好之前被写入。但我们不在乎!即我们假设 bar 没有改变共享数据。

现在 threadB() 需要知道当我们说“准备好”时,我们真正的意思是准备好。所以我们做了一个 read_acquire(foo->ready) .这个读取被添加到读取队列,然后队列被刷新。请注意 w = some_global也可能仍在队列中。所以 foo->ready 可能会在 some_global 之前被读取.但同样,我们不在乎,因为它不是我们如此小心的重要数据的一部分。
我们真正关心的是 foo->x/y/z。所以它们在获取flush/marker之后被加入到读队列中,保证只有在读完foo->ready之后才被读取。

另请注意,这通常与用于锁定和解锁互斥锁/CriticalSection/等的屏障完全相同。 (即在 lock() 上获取,在 unlock() 上释放)。

所以,
  • 我很确定这(即获取/释放)正是 MS 文档所说的在 C# 中读取/写入“ volatile ”变量时发生的情况(以及可选的 MS C++,但这是非标准的)。见 http://msdn.microsoft.com/en-us/library/aa645755(VS.71).aspx包括“ volatile 读取具有“获取语义”;也就是说,它保证发生在对它之后发生的任何内存引用之前......”
  • 我认为java是一样的,虽然我不太熟悉。我怀疑它完全相同,因为您通常不需要比读取-获取/写入-释放更多的保证。
  • 在您的问题中,当您认为这实际上完全与相对顺序有关时,您走在正确的轨道上-您只是将顺序倒过来了(即“读取的值至少与屏障之前的读取一样最新? “- 不,屏障之前的读取不重要,它在屏障之后的读取保证在之后,反之亦然。
  • 请注意,如上所述,重新排序在读取和写入时都会发生,因此仅在一个线程上使用屏障而不是另一个线程将不起作用。即没有读取获取,写入释放是不够的。即,即使您以正确的顺序编写它,如果您没有使用读取屏障来配合写入屏障,它也可能以错误的顺序被读取。
  • 最后,请注意无锁编程和 CPU 内存架构实际上可能比这复杂得多,但坚持使用获取/释放会让你走得很远。
  • 关于multithreading - 我如何理解读取内存障碍和 volatile ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1787450/

    相关文章:

    java - Java 中的 SQLite 连接池 - 锁定数据库

    iphone - (iphone)pushViewController 在后台线程中?

    java - 使用 volatile 发布不可变对象(immutable对象)安全吗?

    java - Java 类加载是单线程的吗?

    c++ - std::condition_variable 内存写入可见性

    c++ - 不同线程数 openMP 上的相同程序执行时间

    c# - 如何使用多线程在 C# 中实现分治算法?

    c++ - C++ 中的 volatile 成员函数与常量成员函数

    assembly - x86 上顺序一致的原子负载

    android - ARM 对 Java 程序的多核惩罚