【问题标题】:How to enforce memory ordering with gcc on x86如何在 x86 上使用 gcc 强制执行内存排序
【发布时间】:2018-06-04 00:31:39
【问题描述】:

我想在线程(gcc、Linux、x86)之间共享一个数据结构。 假设我在线程 A 中有以下代码:

shared_struct->a = 1;
shared_struct->b = 1;
shared_struct->enable = true;

线程 B 是一个周期性任务,它首先检查该结构的 enable 标志。

我认为编译器可以重新排序线程 A 中的写入,因此线程 B 可以看到不一致的数据。我熟悉 ARM 上的内存屏障,但是如何确保 x86 上的写入顺序?有没有比 volatile 更好的方法?

我只想在结构中设置一个一致的状态,将所有内容“刷新”到内存并在最后设置一个启用标志。

【问题讨论】:

  • 根本不是 volatile 的用途。如果有原子标志,则应使用 C11 <stdatomic.h>
  • 我知道 - 这是一个“副产品”,易失性访问不会重新排序。
  • @AnttiHaapala 原子访问与指令重新排序有什么关系?
  • @Lundin volatile 确实保证写入顺序和读取顺序,但不保证一致性
  • 如果您在结构中将abenable 标记为易失性,则保证每个赋值语句都按照它们出现的顺序执行。每个赋值都是一个完整的表达式,末尾有一个序列点。该标准规定,可变访问不能跨序列点重新排序。如果它们被标记为 volatile,则这些分配将按顺序进行

标签: c gcc x86 pthreads memory-barriers


【解决方案1】:

如果您只需要能够设置enable = true,那么stdatomic.h 带有发布/获取顺序就可以满足您的要求。 (在 x86 asm 中,正常的存储/加载具有释放/获取语义,所以是的,阻塞编译时重新排序就足够了。但正确的方法是使用 atomic,而不是 volatile。)

但是如果您希望能够在修改enable = false 时再次“锁定”阅读器,那么您需要一个更复杂的更新模式。要么用原子手动重新发明互斥锁(坏主意;使用标准库互斥锁代替),要么做一些事情,当没有编写器处于更新过程中时,允许多个读取器进行无等待的只读访问。

RCU a 或 seqlock 在这里都可以

对于 seqlock,您有一个序列号,而不是 enable = true/false 标志。读取器可以通过在读取其他成员之前和之后再次检查序列号来检测“撕裂”写入。 (但是所有成员都必须是atomic,至少使用mo_relaxed,否则即使您丢弃该值,只要在C中读取它们就是数据竞争未定义的行为。您还需要对检查的负载进行足够的排序计数器。例如,可能在第一个上获取,然后在 shared_struct->b 加载时获取,以确保序列号的第二次加载在它之后排序。(acquire 只是一个单向障碍:获取加载之后轻松的负荷无法满足您的需求。)

RCU 使阅读器始终完全无需等待;他们只是取消引用指向当前有效结构的指针。更新就像原子地替换指针一样简单。回收旧结构是复杂的地方:在重用它之前,您必须确保每个读取器线程都已完成读取内存块。


在更改其他结构成员之前简单地设置enable = false 不会阻止读者看到enable == true,然后在作者修改它们时看到其他成员的不一致/部分更新的值。如果您不需要这样做,但只释放新对象以供其他线程访问,那么您描述的序列对atomic_store_explicit(&foo->enable, true, memory_order_release) 很好。

【讨论】:

  • memory_order_release 存储应该与阅读器中启用标志的memory_order_acquire 负载配对,对吧?
  • @caf:是的。获取操作(和 seq_cst)操作“同步”释放(和 seq_cst)操作。 preshing.com/20120913/acquire-and-release-semantics 有一些很好的细节,说明它在某些真实机器上的实际工作原理,以及它在订购方面的作用/不给你什么。
【解决方案2】:

你真的应该使用互斥体(因为你提到了Pthread),所以在shared_struct 中添加一个pthread_mutex_lock mtx; 字段(不要忘记用pthread_mutex_init 初始化它)然后

pthread_mutex_lock(&shared_struct->mtx);
shared_struct->a = 1;
shared_struct->b = 1;
shared_struct->enable = true;
pthread_mutex_unlock(&shared_struct->mtx);

在访问该共享数据的任何其他代码中也是如此。

您也可以查看atomic operations(但在您的情况下,您最好使用mutex,如上所示)。

阅读一些pthread tutorial

避免使用race conditionsundefined behavior

如何确保写入顺序

你不要这样做,除非你正在实现一个线程库(然后它的某些部分应该在汇编器中编码并使用futex(7)),例如nptl(7) 的实现GNU 中的 987654328@ glibc(或 musl-libc)。您应该使用互斥锁,并且不想浪费时间来实现线程库(所以使用现有的)。

请注意,Linux 上的大多数 C 标准库(包括 glibc 和 musl-libc)都是 free software,因此您可以研究它们的源代码(如果您想了解 Pthread 互斥锁是如何实现的,等等)。

编译器可以重新排序写入

主要不是(当然不仅是)编译器,而是硬件。阅读cache coherence。并且操作系统也可能参与其中(futex(2) 有时由 pthread 互斥例程调用)。

【讨论】:

  • 谢谢 - 我忘记了明显的互斥锁。
  • 附带说明,volatile 确实防止硬件重新排序。硬件和编译器都不允许偏离 C 规范。硬件对指令进行重新排序以使易失性变量未按照程序员指定的顺序排序的系统不符合要求。如果硬件和编译器都不能保证 C 程序按照 C 标准的“抽象机器”的要求执行,那么用 C 编写的程序就不能在那个系统上使用。将处理内存屏障的负担放在程序员身上并不能使系统符合要求。
  • A pthread_rwlock 也适用于此,IMO 更适合用例,允许多个读取器线程不会相互干扰。
  • @Lundin 硬件和编译器都不允许偏离 C 规范。 好的,这让我笑了,带来了“编译器警察”的图像。
  • 不仅硬件可以重新排序内存访问,整个实现也是如此。在 x86 上,对于普通写入,是编译器对它们进行重新排序,而不是硬件,因为 x86 是 TSO。
猜你喜欢
  • 1970-01-01
  • 2016-12-09
  • 2017-08-27
  • 1970-01-01
  • 2016-11-26
  • 1970-01-01
  • 2017-11-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多