4

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

这是一个最小的工作示例,不幸的是根据第 5.1.2.4 节第 25 节产生未定义的行为,因为数据竞争ready

#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 程序包含所谓的良性竞争时,很多 事情都会出错,所以我真的在寻找合理的编译器和架构可以对上述代码做什么的细节,而不是关于数据竞争和未定义的一般警告行为。

4

1 回答 1

3

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

您使用栅栏的方式是正确的,但是如果您希望能够推断程序行为,您有责任确保在 store(1) toready 和 load(1) from之间存在严格的线程间修改顺序它。这通常是atomic变量发挥作用的地方。根据 C11 标准,您有一个数据竞赛ready(正如您所指出的),并且您可以期待未定义的行为。

我的具体问题是是否可以修复上述代码以保证打印 1 而无需更改为 _Atomic。(我可以准备一个 volatile,但在规范中没有看到任何建议这会有所帮助。)

符合标准的答案是“否”,由于该标准不支持您的案例,您volatile在此上下文中找不到任何相关内容。

但是,考虑到目标之一是支持与许多体系结构的兼容性,该标准是有目的的。这并不意味着数据竞赛总是会导致每个平台出现问题。

不过,在共享上下文中使用非原子类型的问题很棘手。int人们有时认为,如果 CPU 对诸如atomic_int. 这是不正确的,因为“原子”是一个具有更广泛影响的概念:

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

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

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

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

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

你肯定想要缓存一致性,因为不支持它的系统是出了名的难以编程。如果没有缓存一致性,您编写它的方式几乎肯定不会工作。

于 2017-02-22T23:11:34.840 回答