14

我正在试验 g++ 和线程消毒剂,我认为我得到了误报。这是真的,还是我犯了一些大错误?

程序(来自 Anthony Williams:C++ Concurrency in Action,第 145 页,清单 5.13)

#include <atomic>
#include <thread>
#include <assert.h>
bool x=false;
std::atomic<bool> y;
std::atomic<int> z;
void write_x_then_y()
{
  x=true;
  std::atomic_thread_fence(std::memory_order_release);
  y.store(true,std::memory_order_relaxed);
}
void read_y_then_x()
{
  while(!y.load(std::memory_order_relaxed));
  std::atomic_thread_fence(std::memory_order_acquire);
  if(x)
    ++z;
}
int main()
{
  x=false;
  y=false;
  z=0;
  std::thread a(write_x_then_y);
  std::thread b(read_y_then_x);
  a.join();
  b.join();
  assert(z.load()!=0);
}

编译:

g++ -o a -g -Og -pthread a.cpp -fsanitize=thread

g++ 版本

~/build/px> g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/6.1.1/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --enable-bootstrap --enable-languages=c,c++,objc,obj-c++,fortran,ada,go,lto --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-plugin --enable-initfini-array --disable-libgcj --with-isl --enable-libmpx --enable-gnu-indirect-function --with-tune=generic --with-arch_32=i686 --build=x86_64-redhat-linux
Thread model: posix
gcc version 6.1.1 20160621 (Red Hat 6.1.1-3) (GCC)

我正进入(状态:

~/build/px> ./a
==================
WARNING: ThreadSanitizer: data race (pid=13794)
  Read of size 1 at 0x000000602151 by thread T2:
    #0 read_y_then_x() /home/ostri/build/px/a.cpp:17 (a+0x000000401014)
    #1 void std::_Bind_simple<void (*())()>::_M_invoke<>(std::_Index_tuple<>) /usr/include/c++/6.1.1/functional:1400 (a+0x000000401179)
    #2 std::_Bind_simple<void (*())()>::operator()() /usr/include/c++/6.1.1/functional:1389 (a+0x000000401179)
    #3 std::thread::_State_impl<std::_Bind_simple<void (*())()> >::_M_run() /usr/include/c++/6.1.1/thread:196 (a+0x000000401179)
    #4 <null> <null> (libstdc++.so.6+0x0000000baaae)

  Previous write of size 1 at 0x000000602151 by thread T1:
    #0 write_x_then_y() /home/ostri/build/px/a.cpp:9 (a+0x000000400fbd)
    #1 void std::_Bind_simple<void (*())()>::_M_invoke<>(std::_Index_tuple<>) /usr/include/c++/6.1.1/functional:1400 (a+0x000000401179)
    #2 std::_Bind_simple<void (*())()>::operator()() /usr/include/c++/6.1.1/functional:1389 (a+0x000000401179)
    #3 std::thread::_State_impl<std::_Bind_simple<void (*())()> >::_M_run() /usr/include/c++/6.1.1/thread:196 (a+0x000000401179)
    #4 <null> <null> (libstdc++.so.6+0x0000000baaae)

  Location is global 'x' of size 1 at 0x000000602151 (a+0x000000602151)

  Thread T2 (tid=13797, running) created by main thread at:
    #0 pthread_create <null> (libtsan.so.0+0x000000028380)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0x0000000badc4)
    #2 main /home/ostri/build/px/a.cpp:26 (a+0x000000401097)

  Thread T1 (tid=13796, finished) created by main thread at:
    #0 pthread_create <null> (libtsan.so.0+0x000000028380)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0x0000000badc4)
    #2 main /home/ostri/build/px/a.cpp:25 (a+0x00000040108a)

SUMMARY: ThreadSanitizer: data race /home/ostri/build/px/a.cpp:17 in read_y_then_x()
==================
ThreadSanitizer: reported 1 warnings

我在更复杂的程序中收到了这个警告,我认为这是我的错误,但现在即使是“教科书程序”也显示相同的行为。是我还是g++(即缺少一些编译器开关)?

更新 取自链接

#if defined(__SANITIZE_THREAD__)
#define TSAN_ENABLED
#elif defined(__has_feature)
#if __has_feature(thread_sanitizer)
#define TSAN_ENABLED
#endif
#endif

#ifdef TSAN_ENABLED
#define TSAN_ANNOTATE_HAPPENS_BEFORE(addr) \
    AnnotateHappensBefore(__FILE__, __LINE__, (void*)(addr))
#define TSAN_ANNOTATE_HAPPENS_AFTER(addr) \
    AnnotateHappensAfter(__FILE__, __LINE__, (void*)(addr))
extern "C" void AnnotateHappensBefore(const char* f, int l, void* addr);
extern "C" void AnnotateHappensAfter(const char* f, int l, void* addr);
#else
#define TSAN_ANNOTATE_HAPPENS_BEFORE(addr)
#define TSAN_ANNOTATE_HAPPENS_AFTER(addr)
#endif

#include <atomic>
#include <thread>
#include <assert.h>
bool x=false;
std::atomic<bool> y;
std::atomic<int> z;
void write_x_then_y()
{
  x=true;
  std::atomic_thread_fence(std::memory_order_release);
  TSAN_ANNOTATE_HAPPENS_BEFORE(&x);
  y.store(true,std::memory_order_relaxed);
}
void read_y_then_x()
{
  while(!y.load(std::memory_order_relaxed));
  std::atomic_thread_fence(std::memory_order_acquire);
  TSAN_ANNOTATE_HAPPENS_AFTER(&x);
  if(x)
    ++z;
}
{
  x=false;
  y=false;
  z=0;
  std::thread a(write_x_then_y);
  std::thread b(read_y_then_x);
  a.join();
  b.join();
  assert(z.load()!=0);
}

编译命令

g++ -o a -g -Og -pthread a.cpp -fsanitize=thread -D__SANITIZE_THREAD__
4

3 回答 3

5

TL;DR:这是 TSAN 误报。代码有效。

线程 1:

  x=true;                                              // W
  std::atomic_thread_fence(std::memory_order_release); // A
  y.store(true,std::memory_order_relaxed);             // X

线程 2:

  while(!y.load(std::memory_order_relaxed));           // Y
  std::atomic_thread_fence(std::memory_order_acquire); // B
  if(x)                                                // R
     ++z;

[atomics.fences]/2

如果存在原子操作 X 和 Y,则释放栅栏 A 与获取栅栏 B 同步,两者都对某个原子对象 M 进行操作,使得 A 在 X 之前排序,X 修改 M,Y 在 B 之前排序,并且 Y 读取值由 X 写入或由假设的释放序列中的任何副作用写入的值 X 将在它是一个释放操作时开始。

让我们浏览一下列表:

  • [✔]“存在原子操作 X 和 Y,都在某个原子对象 M 上进行操作”:很明显。米是y
  • [✔]“A 在 X 之前排序”:很明显([intro.execution]/14对于那些想要引用的人)。
  • [✔]“X修改M”:很明显。
  • [✔]“Y 在 B 之前排序”:很明显。
  • [✔]“并且 Y 读取 X 写入的值...”:这是循环终止的唯一方法。

因此,释放栅栏 A 与获取栅栏 B 同步。

写入 W 在 A 之前排序,读取 R 在 B 之后排序,因此 W线程间发生在 之前,因此发生在R之前。 [intro.races]/9-10

评估 A线程间发生在评估 B 之前,如果

  • A 与 B 同步,或
  • A 在 B 之前是依赖排序的,或者
  • 对于一些评估 X
    • A 与 X 同步并且 X 在 B 之前排序,或者
    • A 在 X 之前排序,并且 X 线程间发生在 B 之前,或者
    • 线程间发生在 X 之前,而 X 线程间发生在 B 之前。

评估 A发生在评估 B 之前(或等效地,B 发生在A 之后),如果:

  • A 在 B 之前排序,或
  • 线程间发生在 B 之前。

由于happens-before关系( [intro.races]/19 ) ,没有数据竞争:

如果程序的执行包含两个潜在的并发冲突操作,则程序的执行包含数据竞争,其中至少一个不是原子的,并且两者都不会在另一个之前发生,除了下面描述的信号处理程序的特殊情况。任何此类数据竞争都会导致未定义的行为。

此外,读取的 R 保证读取 W 写入的值,因为 W 是可见的副作用x,线程启动后没有其他副作用( [intro.races]/11):

相对于 M 的值计算 B,标量对象或位域 M 上的可见副作用 A 满足以下条件:

  • A发生在B之前并且
  • X 对 M 没有其他副作用,即 A 发生在 X 之前,X 发生在 B 之前。

由评估 B 确定的非原子标量对象或位域 M 的值应为可见副作用 A 存储的值。

于 2016-08-15T21:04:27.123 回答
1

memory_order_relaxed对重新排序没有任何限制。

memory_order_acquire不会阻止从上方重新排序越过栅栏。它只会阻止从下面订购。这意味着代码可以像这样执行:

std::atomic_thread_fence(std::memory_order_acquire);
if(x)
  ++z;
while(!y.load(std::memory_order_relaxed));

这将导致数据竞争,因为读取if(x)x=true.

memory_order_acq_rel您需要在两个函数中使用或语义的栅栏,以memory_order_seq_cst防止在两个方向上重新排序。

于 2016-08-15T10:29:55.320 回答
1

不幸的是 ThreadSanitizer 无法理解内存栅栏。这是因为它根据对某些对象的访问之间的发生之前的关系进行推理,并且在栅栏操作中没有对象。

如果您用获取负载替换宽松加载 + 获取栅栏,并将释放栅栏 + 宽松存储替换为释放存储,TSan 将正确检测存储和加载之间的发生前关系。

另请注意,GCC 的 TSan 实现可能无法在 O0 处检测原子(请参阅https://stackoverflow.com/a/42905055/234420)。

于 2017-03-23T16:48:23.860 回答