Multithreading 我如何理解读记忆障碍和易失性

Multithreading 我如何理解读记忆障碍和易失性,multithreading,volatile,memory-barriers,Multithreading,Volatile,Memory Barriers,一些语言提供了一个volatile修饰符,该修饰符被描述为在读取支持变量的内存之前执行“读取内存屏障” 读存储器屏障通常被描述为确保CPU在屏障之后执行读取请求之前,已经在屏障之前执行了请求的读取。然而,使用这个定义,似乎仍然可以读取过时的值。换言之,以特定顺序执行读取似乎并不意味着必须参考主存储器或其他CPU,以确保后续读取的值实际反映系统中在读取屏障时的最新值,或在读取屏障后后续写入的值 那么,volatile真的能保证读取最新的值吗?或者(喘息!)所读取的值至少与屏障之前的读取一样是最新的

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

读存储器屏障通常被描述为确保CPU在屏障之后执行读取请求之前,已经在屏障之前执行了请求的读取。然而,使用这个定义,似乎仍然可以读取过时的值。换言之,以特定顺序执行读取似乎并不意味着必须参考主存储器或其他CPU,以确保后续读取的值实际反映系统中在读取屏障时的最新值,或在读取屏障后后续写入的值


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

在大多数编程语言中,volatile并不意味着真正的CPU读内存障碍,而是编译器不通过寄存器中的缓存优化读操作的命令。这意味着读取进程/线程将获得“最终”值。一种常见的技术是声明一个布尔
volatile
标志,该标志将在信号处理程序中设置并在主程序循环中检查。

相反,CPU内存障碍可以通过CPU指令直接提供,也可以通过某些汇编程序助记符(例如x86中的
lock
前缀)暗示例如,当与硬件设备交谈时,读取和写入内存映射IO寄存器的顺序非常重要,或者在多处理环境中同步内存访问时,使用and。

回答您的问题-不,内存屏障不保证“最新”值,但保证内存访问操作的顺序。例如,在编程中,这是至关重要的。


是CPU内存屏障的引物之一。

有读屏障和写屏障;获取障碍并释放障碍。以及更多(io与内存等)

这些障碍不是为了控制价值观的“最新”或“新鲜度”。它们用于控制内存访问的相对顺序

写入屏障控制写入顺序。因为写入内存的速度很慢(与CPU的速度相比),所以通常会有一个写入请求队列,在写入“真正发生”之前就在该队列中发布。虽然它们是按顺序排队的,但在队列内写入操作可能会重新排序。(所以可能“queue”不是最好的名称…)除非您使用写屏障来阻止重新排序

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

无论编译器试图对生成代码的顺序做什么,这都是正确的。这里的“易失性”在C++中是无济于事的,因为它只告诉编译器输出代码来从“内存”重新读取值,它不告诉CPU如何/从哪里读取它(即“内存”是CPU级别的很多东西)。 因此,读/写屏障设置了块来防止读/写队列中的重新排序(读通常不是一个队列,但重新排序的效果是相同的)

什么样的积木获取和/或释放块

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

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

请注意,我将读取与获取配对,将写入与释放配对,因为这是典型的情况,但可以使用不同的组合

获取和释放被视为“半屏障”或“半围栏”,因为它们只会阻止重新排序的单向进行

完全隔离(或完全隔离)适用于获取和释放-即不重新排序

通常,对于无锁编程、C#或java“volatile”,您需要的是 读获取和写释放

因此,首先,这是一种不好的线程编程方法。锁会更安全。但为了说明障碍

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

现在threadB()需要知道,当我们说“ready”时,实际上是指ready。所以我们做了一个
读取(foo->ready)
。将此读取添加到读取队列,然后刷新队列。请注意,
w=some_global
也可能仍在队列中。因此,foo->ready可能会在
某些\u global
之前读取。但同样,我们不在乎,因为这不是我们如此小心处理的重要数据的一部分。 我们真正关心的是foo->x/y/z。因此,它们被添加到read q中
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();
}