为什么线程清理程序会抱怨获取/释放线程栅栏?

Hol*_*Cat 21 c++ atomic memory-barriers stdatomic thread-sanitizer

我正在学习不同的记忆顺序。

\n

我有这段代码,它可以工作并通过 GCC 和 Clang 的线程清理程序

\n
#include <atomic>\n#include <iostream>\n#include <future>\n    \nint state = 0;\nstd::atomic_int a = 0;\n\nvoid foo(int from, int to) \n{\n    for (int i = 0; i < 10; i++)\n    {\n        while (a.load(std::memory_order_acquire) != from) {}\n        state++;\n        a.store(to, std::memory_order_release);\n    }\n}\n\nint main()\n{    \n    auto x = std::async(std::launch::async, foo, 0, 1);\n    auto y = std::async(std::launch::async, foo, 1, 0);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

我认为如果它最终没有返回,则“获取”加载是不必要的from,那么“获取”负载是不必要的,因此我决定使用“宽松”负载,然后使用“获取”栅栏。

\n

我期望它能工作,但它被线程清理程序拒绝了,线程清理程序声称并发state++是数据竞争。

\n
#include <atomic>\n#include <iostream>\n#include <future>\n    \nint state = 0;\nstd::atomic_int a = 0;\n\nvoid foo(int from, int to) \n{\n    for (int i = 0; i < 10; i++)\n    {\n        while (a.load(std::memory_order_relaxed) != from) {}\n        std::atomic_thread_fence(std::memory_order_acquire);\n        state++;\n        a.store(to, std::memory_order_release);\n    }\n}\n\nint main()\n{    \n    auto x = std::async(std::launch::async, foo, 0, 1);\n    auto y = std::async(std::launch::async, foo, 1, 0);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

为什么这是一场数据竞赛?

\n

Cppreference

\n
\n

原子栅栏同步

\n

线程 A 中的原子释放操作 X 与线程 B 中的获取栅栏 F 同步,如果

\n
    \n
  • 存在原子读取 Y(具有任何内存顺序)
  • \n
  • Y读取X(或以X为首的释放序列)写入的值
  • \n
  • 在线程 B 中,Y 排序在 F 之前
  • \n
\n

在这种情况下,线程 A 中 X 之前排序的所有非原子和宽松原子存储将发生在线程 B 中 F 之后的相同位置的所有非原子和宽松原子加载之前。

\n
\n

据我了解,满足所有条件:

\n
    \n
  • “存在原子读取 Y(具有任何内存顺序)” \xe2\x80\x94 检查:a.load(std::memory_order_relaxed)
  • \n
  • "Y 读取 X 写入的值" \xe2\x80\x94 检查,它从 读取值a.store(to, std::memory_order_release);
  • \n
  • “Y 在线程 B 中排序在 F 之前”\xe2\x80\x94 检查。
  • \n
\n

Hol*_*Cat 19

线程清理程序当前不支持std::atomic_thread_fence. (GCC 和 Clang 使用相同的线程清理程序,因此它适用于两者。)

GCC 12(当前主干)对此发出警告:

atomic_base.h:133:26: warning: 'atomic_thread_fence' is not supported with '-fsanitize=thread' [-Wtsan]
  133 |   { __atomic_thread_fence(int(__m)); }
      |     ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~
Run Code Online (Sandbox Code Playgroud)

要阻止消毒剂忽略围栏,您可以使用__tsan_acquire和手动检测它们__tsan_release

#include <sanitizer/tsan_interface.h>

while (a.load(std::memory_order_relaxed) != from) {}
__tsan_acquire(&a); // <--
std::atomic_thread_fence(std::memory_order_acquire);
Run Code Online (Sandbox Code Playgroud)

我认为自动确定哪些原子变量受到栅栏影响是很棘手的。

尽管错误报告说seq_cst栅栏不受影响__tsan_acquire,但如果我使用这样的栅栏,代码仍然会被拒绝,它们仍然需要用+注释__tsan_release与 acq-rel fences 完全相同