c++ 11 (c++线程净化)用原子排序非原子操作(误报?)
C++11 (g++ thread sanitized) Ordering nonatomic operations with atomics (false positive?)
我正在试验g++和线程消毒程序,我认为我得到了假阳性。这是真的吗,还是我犯了什么大错?
程序(剪切&粘贴自Anthony Williams: c++ Concurrency in Action, page 145, listing 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__
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在X之前排序B, Y读取X写的值或者任意一边写的值在假设的释放序列中,如果X是a,那么它将指向释放操作。
让我们浏览一下列表:
- [✔] "存在原子操作X和Y,它们都在某个原子对象M上操作":很明显。M为
y
。 - [✔]"A在X之前排序":obvious ([intro.]
- [✔] "X modifiers M": obvious.
- [✔] "Y在B之前排序":显而易见。
- [✔] "and 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之前,或者
- A线程间发生在X之前,X线程间发生在b之前
求值A 发生在求值B(或等价地,B)之前发生在之后A) if:
- A在B之前排序,或
- 线程间发生在b之前
由于happens-before关系([intro.races]/19):
如果程序包含两个数据竞争,则该程序的执行包含数据竞争潜在的并发冲突行为,其中至少有一个是不是原子的,两者都不发生在另一个之前,除了下面描述的信号处理程序的特殊情况。任何这样的数据竞赛导致未定义的行为。
而且,读R保证读W写的值,因为W是可见的副作用,在线程启动([intro.races]/11)之后,对x
没有其他副作用:
对标量对象或位域M的可见副作用A计算B (M)满足以下条件:
- A发生在B之前,
- 不存在X对M的其他副作用,使得A发生在X之前,X发生在b之前
非原子标量对象或位域M的值,由确定通过求值B,为可见副作用存储的值a .
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
语义围栏,以防止在两个方向上重新排序。
不幸的是,ThreadSanitizer无法理解内存栅栏。这是因为它根据访问特定对象之间的happens-before关系进行推理,而fence操作中没有对象。
如果将松弛加载+获取栅栏替换为获取负载,将释放栅栏+放松存储替换为释放存储,则TSan将正确检测存储和负载之间的happens-before关系。
还要注意,GCC的TSan实现可能无法在0处检测原子(参见https://stackoverflow.com/a/42905055/234420)。
- 二叉排序树无法编译
- 仅使用绝对值对数组进行排序,并在C++中显示实际值
- C++选择排序算法中的逻辑错误
- 使用C++程序合并排序没有得到正确的输出
- 计算排序向量的向量中唯一值的计数
- 排序算法c++
- 使用2个键的cpp-stl::优先级队列排序不正确
- 将结构向量排序为子组
- 在c++中尝试对对象数组进行排序时,出现std:bad_alloc错误
- 如何对点云数据进行排序
- 对字符串进行排序时,在c++中处理sort()
- 是否有类似std::lower_bound的函数,而不需要排序/分区输入
- 下面是排序算法O(n)吗
- std::sort()函数无法对向量的一部分进行排序
- shell排序中的交换和比较
- clang格式:禁用排序包含
- 显示错误输出的简单数组排序程序
- 为什么我的排序算法会更改数组值
- 试图在c++中对数字列表进行排序
- 如何在C++中对数组进行冒泡排序