C ++ 11(g ++线程已消毒)使用原子对非原子操作进行排序(误报?)

时间:2016-08-15 09:43:44

标签: c++ multithreading c++11 thread-sanitizer

我正在尝试使用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

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

已更新 取自link

#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__

3 个答案:

答案 0 :(得分: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

  

释放栏A如果存在,则与获取栏B同步   原子操作X和Y,都在一些原子对象M上运行,   这样A在X之前被测序,X修改M,Y在之前被测序   B和Y读取由X写入的值或由任何一方写入的值   在假设的释放序列中,如果它是a,则X将会结束   释放操作。

让我们看看清单:

  • [✔]&#34;存在原子操作X和Y,两者都在某些原子对象M&#34;上运行:显而易见。 M是y
  • [✔]&#34; A在X&#34之前排序:显而易见([intro.execution]/14对于那些想要引用的人来说。)
  • [✔]&#34; X修改M&#34;:显而易见。
  • [✔]&#34; Y在B&#34;之前排序:显而易见。
  • [✔]&#34;并且Y读取X写的值......&#34;:这是循环终止的唯一方式。

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

写入W在A之前排序,读取R在B之后排序,因此W 线程发生在之前,因此发生在,R之前。 {3}}:

  

评估A 内部线程发生在评估B之前

     
      
  • 与B同步,或
  •   
  • A是在B之前依赖订购的,或
  •   
  • 进行一些评估X.      
        
    • 与X同步,X在B之前排序,或
    •   
    • 在X和X之间的线程发生在B之前或
    • 之前对A进行排序   
    • 在线程发生之前,X和X线程发生在B之前。
    •   
  •   
     

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

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

由于之前发生的关系([intro.races]/9-10)没有数据竞争:

  

如果程序包含两个,则程序的执行包含数据竞争   潜在的并发冲突行为,其中至少有一个是   不是原子的,也没有发生在另一个之外,除了   下面描述的信号处理程序的特殊情况。任何这样的数据竞争   导致未定义的行为。

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

  

相对于标量对象或位字段M的可见副作用A.   到M的值计算B满足条件:

     
      
  • A发生在B和
  • 之前   
  • 没有其他副作用X到M,使得A发生在X之前,而X发生在B之前。
  •   
     

确定的非原子标量对象或位域M的值   通过评估B,应该是由可见副作用存储的值   甲

答案 1 :(得分: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_relmemory_order_seq_cst语义的围栏,以防止在两个方向上重新排序。

答案 2 :(得分:0)

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

如果您使用获取负载替换宽松负载+获取围栏,并使用释放存储替换释放围栏+宽松存储,则TSan将正确检测存储与负载之间发生的关系。

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