c++ - ThreadSanitizer reports "data race on operator delete(void*)" when using embedded reference counter -
please have @ following code:
#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(); // thread1 obj->reserve(); // thread2 obj->release(); // 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); } this similar reference counting example boost.atomic.
the main differences embedded ref_count_ initialized 1 in constructor (once constructor completed have single reference referencecounted object) , code doesn't use boost::intrusive_ptr.
please don't blame me using delete this in code - pattern have in large code base @ work , there's nothing can right now.
now code compiled clang 3.5 trunk (details below) , threadsanitizer (tsan v2) results in following output threadsanitizer:
warning: threadsanitizer: data race (pid=9871) write of size 1 @ 0x7d040000f7f0 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 @ 0x7d040000f7f0 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 heap block of size 4 @ 0x7d040000f7f0 allocated 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 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 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 the strange thing thread t1 write of size 1 same memory location thread t2 when doing atomic decrement on reference counter.
how can former write explained? clean-up performed destructor of referencecounted class?
it false positive? or code wrong?
my setup is:
$ 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 the code compiled this:
clang++ main.cpp -i/home/a.romanek/tmp/boost/boost_1_55_0 -pthread -fsanitize=thread -o0 -g -ggdb3 -fpie -pie -fpic note on machine implementation of boost::atomic<t> resolves __atomic_load_n family of functions, threadsanitizer claims understand.
update 1: same happens when using clang 3.4 final release.
update 2: same problem occurs -std=c++11 , <atomic> both libstdc++ , libc++.
this looks false positive.
the thread_fence in release() method enforces outstanding writes fetch_sub-calls happen-before fence returns. therefore, delete on next line cannot race previous writes decreasing refcount.
quoting book c++ concurrency in action:
a release operation synchronizes-with fence
orderofstd::memory_order_acquire[...] if release operation stores value that's read atomic operation prior fence on same thread fence.
since decreasing refcount read-modify-write operation, should apply here.
to elaborate, order of operations need ensure follows:
- decreasing refcount value > 1
- decreasing refcount 1
- deleting object
2. , 3. synchronized implicitly, happen on same thread. 1. , 2. synchronized since both atomic read-modify-write operations on same value. if these 2 race whole refcounting broken in first place. left synchronizing 1. , 3..
this fence does. write 1. release operation is, discussed, synchronized 2., read on same value. 3., acquire fence on same thread 2., synchronizes write 1. guaranteed spec. happens without requiring addition acquire write on object (as suggested @kerreksb in comments), work, might potentially less efficient due additional write.
bottom line: don't play around memory orderings. experts them wrong , impact on performance negligible. unless have proven in profiling run kill performance , absolutely positively have to optimize this, pretend don't exist , stick default memory_order_seq_cst.
Comments
Post a Comment