在C11中具有非原子的栅栏

时间:2017-02-08 20:05:14

标签: c11 memory-model memory-fences

有没有办法使用围栏来推断C11中非原子操作的行为?具体来说,我想在某些字段需要int以便与旧接口兼容的情况下使代码安全,这些旧接口可能会读取和写入数据结构到文件或将它们作为系统调用参数传递。由于并不要求atomic_int的大小与int的大小相同,因此我无法使用atomic_int

这是一个最小的工作示例,由于ready上的数据竞争,遗憾的是根据第5.1.2.4节第25​​段产生未定义的行为:

#include <stdatomic.h>
#include <stdio.h>
#include <threads.h>

int ready;  /* purposely NOT _Atomic */
int value;

void
p1()
{
  value = 1;
  atomic_thread_fence(memory_order_release);
  ready = 1;
}

void
p2(void *_ignored)
{
  while (!ready)
    ;
  atomic_thread_fence(memory_order_acquire);
  printf("%d\n", value);
}

int
main()
{
  thrd_t t;
  thrd_create(&t, p2, NULL);
  p1();
  thrd_join(&t, NULL);
}

我的具体问题是,是否可以修复上述代码以保证打印1而不将ready更改为_Atomic。 (我可以将ready设为volatile,但在规范中没有看到任何可能有帮助的建议。)

一个相关的问题是,无论如何编写上面的代码是否安全,因为我的代码运行的任何机器都具有缓存一致性?当C11程序包含所谓的良性竞赛时,我知道many thingswrong,所以我真的在寻找一个合理的编译器和架构可以做什么的具体细节上面的代码而不是关于数据争用和未定义行为的一般警告。

1 个答案:

答案 0 :(得分:2)

  

有没有办法使用围栏来推断C11中非原子操作的行为?

你使用栅栏的方式是正确的,但如果你想能够推理程序行为, 您有责任确保商店(1)到ready之间存在严格的线程间修改顺序 和负载(1)。这通常是atomic变量发挥作用的地方。 根据C11标准,您在ready上进行了数据竞争(正如您所指出的那样),未定义的行为就是您所期望的。

  

我的具体问题是,是否可以修复上述代码以保证打印1而不将准备更改为_Atomic。    (我可以准备一个不稳定的,但不要在规范中看到任何有助于此的建议。)

符合标准的答案是&#39; no&#39;由于该标准不支持您的案例,因此您无法在此上下文中找到与volatile相关的任何内容。

然而,考虑到其中一个目标是支持与许多架构的兼容性,该标准是有目的的。 这并不意味着数据竞争总会导致每个平台出现问题。

但是在共享上下文中使用非原子类型的问题很棘手。 人们有时会认为,如果int等类型的CPU操作不可分割,则可以将其用作atomic_int的替代。 这不是真的,因为&#39; atomic&#39;是一个具有更广泛影响的概念:

  • 不可分割的读/写 - 这些适用于许多平台上的常规类型。

  • 受限制的优化 - 编译器转换可以以许多意想不到的方式真正导致未定义的行为。 编译器可以重新排序内存操作,将变量与另一个变量组合在同一个内存位置, 从循环中删除变量,将其保存在寄存器中等等...... 您可以通过声明变量volatile来防止这种情况,因为它会限制编译器可以执行的优化。

  • 核心之间的数据同步 - 在您的情况下,这是由围栏处理的,条件是在存储和负载之间ready上存在严格的线程间排序。 使用真实的atomic_int,您可以使用轻松的操作。

您的代码是否有效,取决于平台和编译器,但至少声明ready标志volatile。 我使用gcc -O3编译器优化在X86_64上进行了测试运行,没有volatile它被无限循环捕获。
比较原子和非原子情况下编译器发出的指令之间的差异也是一个好主意。

  

一个相关的问题是,无论如何编写上述代码是否安全,因为我的代码运行的任何机器都具有缓存一致性?

你肯定想要缓存一致性,因为不支持它的系统非常难以编程。如果没有缓存一致性,你编写它的方式几乎肯定不会起作用。