代码之家  ›  专栏  ›  技术社区  ›  Jason Kresowaty

我如何理解阅读记忆障碍和易变

  •  54
  • Jason Kresowaty  · 技术社区  · 15 年前

    有些语言提供 volatile 在读取返回变量的内存之前执行“读取内存屏障”的修饰符。

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

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

    2 回复  |  直到 9 年前
        1
  •  105
  •   tony    9 年前

    有读障碍和写障碍;获取障碍和释放障碍。更多(IO与内存等)。

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

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

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

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

    因此,读/写屏障会设置块来防止读/写队列中的重新排序(读通常不太像队列,但重新排序效果相同)。

    什么样的街区?-获取和/或释放块。

    获取-例如,读取获取(X)会将X的读取添加到读取队列中。 冲洗队列 (不是真正刷新队列,而是添加一个标记,表示在此次读取之前不要对任何内容重新排序,这就好像刷新了队列一样)。所以以后(按代码顺序)的读取可以重新排序,但不能在X的读取之前。

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

    请注意,我将read与acquire和write与release配对,因为这是典型的,但可能有不同的组合。

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

    一个完整的屏障(或完整的屏障)同时应用一个获得和一个释放-即没有重新排序。

    通常用于无锁编程,或C或Java的“易失性”,您想要/需要的是 读取获取和写入释放。

    工业工程

    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,really last,否则其他线程可能会看到foo->ready early,并获取错误的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->就绪 之前 some_global . 但是,我们也不在乎,因为这不是我们如此小心处理的重要数据的一部分。 我们关心的是foo->x/y/z。因此,在获取刷新/标记之后,它们将被添加到读取队列中,确保它们在读取foo->就绪之后才是只读的。

    还要注意,这通常是用于锁定和解锁互斥体/关键部分/etc的完全相同的屏障(即acquire on lock(),release on unlock())。

    所以,

    • 我很肯定这(即获取/发布)正是MS文档所说的“C”中的“易失性”变量的读取/写入(以及可选的MS C++,但这是非标准的)。见 http://msdn.microsoft.com/en-us/library/aa645755(VS.71).aspx 包括“volatile read具有”acquire semantics“;也就是说,它保证在引用之后发生的任何内存之前发生…”

    • 认为 Java是一样的,虽然我不是那么熟悉。我怀疑这是完全相同的,因为您通常不需要比读获取/写发布更多的保证。

    • 在你的问题中,当你认为这实际上是关于相对顺序的时候,你是在正确的轨道上的——你只是把顺序向后(即“读取的值至少和屏障前的读取一样是最新的?”-不,读之前的屏障是不重要的,它的读之后的屏障是保证之后,反之亦然。

    • 请注意,正如前面提到的,重新排序同时发生在读和写上,所以只有在一个线程上使用一个屏障,而不是在另一个线程上使用屏障是不起作用的。如果没有读获取,写发布就不够了。也就是说,即使你写的顺序是正确的,如果你不使用读障碍来配合写障碍,它也可能被错误的顺序读。

    • 最后,请注意,无锁编程和CPU内存体系结构实际上可能比这复杂得多,但是坚持使用Acquire/Release会让您走得很远。

        2
  •  9
  •   Nikolai Fetissov    15 年前

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

    相反,CPU内存屏障是通过CPU指令直接提供的,或者是通过某些汇编程序助记法(例如 lock 在x86中使用前缀),例如,当与硬件设备通信时,对内存映射的IO寄存器的读写顺序很重要,或者在多处理环境中同步内存访问时使用前缀。

    回答你的问题-不,记忆障碍不保证“最新”的价值,但保证 秩序 内存访问操作。这是至关重要的,例如 lock-free 编程。

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