Ada*_*nek 5 c++ multithreading boost reference-counting thread-sanitizer
请看下面的代码:
#include <pthread.h>
#include <boost/atomic.hpp>
class ReferenceCounted {
public:
ReferenceCounted() : ref_count_(1) {}
void reserve() {
ref_count_.fetch_add(1, boost::memory_order_relaxed);
}
void release() {
if (ref_count_.fetch_sub(1, boost::memory_order_release) == 1) {
boost::atomic_thread_fence(boost::memory_order_acquire);
delete this;
}
}
private:
boost::atomic<int> ref_count_;
};
void* Thread1(void* x) {
static_cast<ReferenceCounted*>(x)->release();
return NULL;
}
void* Thread2(void* x) {
static_cast<ReferenceCounted*>(x)->release();
return NULL;
}
int main() {
ReferenceCounted* obj = new ReferenceCounted();
obj->reserve(); // for Thread1
obj->reserve(); // for Thread2
obj->release(); // for the main()
pthread_t t[2];
pthread_create(&t[0], NULL, Thread1, obj);
pthread_create(&t[1], NULL, Thread2, obj);
pthread_join(t[0], NULL);
pthread_join(t[1], NULL);
}
Run Code Online (Sandbox Code Playgroud)
这有点类似于Boost.Atomic中的Reference计数示例.
主要区别在于嵌入式ref_count_被初始化为1构造函数(一旦构造函数完成,我们对ReferenceCounted对象有一个引用)并且代码不使用boost::intrusive_ptr.
请不要责怪我delete this在代码中使用- 这是我在工作中的大型代码库中的模式,现在我无能为力.
现在,clang 3.5使用trunk(下面的详细信息)和ThreadSanitizer(tsan v2)编译的代码会导致ThreadSanitizer的以下输出:
WARNING: ThreadSanitizer: data race (pid=9871)
Write of size 1 at 0x7d040000f7f0 by thread T2:
#0 operator delete(void*) <null>:0 (a.out+0x00000004738b)
#1 ReferenceCounted::release() /home/A.Romanek/tmp/tsan/main.cpp:15 (a.out+0x0000000a2c06)
#2 Thread2(void*) /home/A.Romanek/tmp/tsan/main.cpp:29 (a.out+0x0000000a2833)
Previous atomic write of size 4 at 0x7d040000f7f0 by thread T1:
#0 __tsan_atomic32_fetch_sub <null>:0 (a.out+0x0000000896b6)
#1 boost::atomics::detail::base_atomic<int, int, 4u, true>::fetch_sub(int, boost::memory_order) volatile /home/A.Romanek/tmp/boost/boost_1_55_0/boost/atomic/detail/gcc-atomic.hpp:499 (a.out+0x0000000a3329)
#2 ReferenceCounted::release() /home/A.Romanek/tmp/tsan/main.cpp:13 (a.out+0x0000000a2a71)
#3 Thread1(void*) /home/A.Romanek/tmp/tsan/main.cpp:24 (a.out+0x0000000a27d3)
Location is heap block of size 4 at 0x7d040000f7f0 allocated by main thread:
#0 operator new(unsigned long) <null>:0 (a.out+0x000000046e1d)
#1 main /home/A.Romanek/tmp/tsan/main.cpp:34 (a.out+0x0000000a286f)
Thread T2 (tid=9874, running) created by main thread at:
#0 pthread_create <null>:0 (a.out+0x00000004a2d1)
#1 main /home/A.Romanek/tmp/tsan/main.cpp:40 (a.out+0x0000000a294e)
Thread T1 (tid=9873, finished) created by main thread at:
#0 pthread_create <null>:0 (a.out+0x00000004a2d1)
#1 main /home/A.Romanek/tmp/tsan/main.cpp:39 (a.out+0x0000000a2912)
SUMMARY: ThreadSanitizer: data race ??:0 operator delete(void*)
==================
ThreadSanitizer: reported 1 warnings
Run Code Online (Sandbox Code Playgroud)
奇怪的是,在参考计数器上执行原子递减时thread T1,将大小1写入相同的内存位置thread T2.
如何解释前者的写作?它是由ReferenceCounted类的析构函数执行的一些清理吗?
这是假阳性?或者代码错了?
我的设置是:
$ uname -a
Linux aromanek-laptop 3.13.0-29-generic #53-Ubuntu SMP Wed Jun 4 21:00:20 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
$ clang --version
Ubuntu clang version 3.5-1ubuntu1 (trunk) (based on LLVM 3.5)
Target: x86_64-pc-linux-gnu
Thread model: posix
Run Code Online (Sandbox Code Playgroud)
代码编译如下:
clang++ main.cpp -I/home/A.Romanek/tmp/boost/boost_1_55_0 -pthread -fsanitize=thread -O0 -g -ggdb3 -fPIE -pie -fPIC
Run Code Online (Sandbox Code Playgroud)
请注意,在我的机器上执行boost::atomic<T>解析为__atomic_load_n函数族,ThreadSanitizer声称理解这些函数.
更新1:使用clang 3.4最终版本时也会发生同样的情况.
更新2:与出现同样的问题-std=c++11,并<atomic>与二者的libstdc ++和的libc ++.
这看起来像是误报。
该方法thread_fence中的release()强制执行所有未完成的写入fetch_sub- 调用发生 - 在栅栏返回之前。因此,delete下一行上的 不能因减少引用计数而与之前的写入竞争。
引用《C++ Concurrency in Action》一书:
如果释放操作存储的值是由与栅栏相同的线程上的栅栏之前的原子操作读取的值,则释放操作与具有
orderof [...] 的栅栏同步。std::memory_order_acquire
由于减少引用计数是一个读取-修改-写入操作,因此这应该适用于此处。
详细来说,我们需要确保的操作顺序如下:
2.并且3.是隐式同步的,因为它们发生在同一个线程上。1.和2.是同步的,因为它们都是对同一值的原子读-修改-写操作。如果这两个人能够比赛,整个重新计票就会首先被打破。那么剩下的就是同步1.和3.。
这正是栅栏的作用。正如我们刚刚讨论的,写入1.操作是与读取同一值同步的操作。,与 位于同一线程上的栅栏,现在与规范所保证的写入同步。这种情况的发生不需要对对象进行额外的写入(正如 @KerrekSB 在评论中所建议的那样),这也可以工作,但由于额外的写入可能会降低效率。release2.3.acquire2.1.acquire
底线:不要玩弄内存顺序。即使专家也会犯错,而且它们对性能的影响通常可以忽略不计。因此,除非您在分析运行中证明它们会影响您的性能,并且您绝对必须对其进行优化,否则就假装它们不存在并坚持使用默认值memory_order_seq_cst。