The code.
1 #include <thread>
2 #include <mutex>
3 #include <condition_variable>
4 #include <iostream>
5
6 using namespace std;
7
8 class A {
9 public:
10 virtual void F() = 0;
11 void Done() {
12 F();
13 lock_guard<mutex> l{m};
14 is_done = true;
15 cv.notify_one();
16 cout << "Called Done...\n";
17 }
18
19 virtual ~A() {
20 unique_lock<mutex> l{m};
21 cout << "Waiting for Done...\n";
22 cv.wait(l, [&] {return is_done;});
23 cout << "Destroying object...\n";
24 }
25
26 private:
27 mutex m;
28 condition_variable cv;
29 bool is_done{false};
30 };
31
32 class B: public A {
33 public:
34 virtual void F() {
35 }
36
37 ~B() {
38 cout << "~B done\n";
39 }
40 };
41
42 int main() {
43 A *obj{new B};
44
45 thread t1{[=] {
46 obj->F();
47 obj->Done();
48 }};
49
50 thread t2{[=] {delete obj;}};
51 t1.join();
52 t2.join();
53
54 return 0;
55 }
ghost@ubuntu:~/work/test$ c34 thread_bug.cxx -fsanitize=thread
ghost@ubuntu:~/work/test$ ./a.out
Called Done...
~B done
==================
WARNING: ThreadSanitizer: data race on vptr (ctor/dtor vs virtual call) (pid=21270)
Write of size 8 at 0x7d1c0000df90 by thread T2:
#0 A::~A() /home/ghost/work/test/thread_bug.cxx:19 (a.out+0x0000000b061e)
#1 B::~B() /home/ghost/work/test/thread_bug.cxx:39 (a.out+0x0000000b055e)
#2 B::~B() /home/ghost/work/test/thread_bug.cxx:37 (a.out+0x0000000b04b7)
#3 main::$_1::operator()() const /home/ghost/work/test/thread_bug.cxx:50 (a.out+0x0000000abd60)
#4 void std::_Bind_simple<main::$_1 ()>::_M_invoke<>(std::_Index_tuple<>) /usr/gcc48/include/c++/4.8.2/functional:1731 (a.out+0x0000000abc70)
#5 std::_Bind_simple<main::$_1 ()>::operator()() /usr/gcc48/include/c++/4.8.2/functional:1720 (a.out+0x0000000abc10)
#6 std::thread::_Impl<std::_Bind_simple<main::$_1 ()> >::_M_run() /usr/gcc48/include/c++/4.8.2/thread:115 (a.out+0x0000000abbb9)
#7 execute_native_thread_routine /home/ghost/work/gcc/gcc-4.8.2/x86_64-unknown-linux-gnu/libstdc++-v3/src/c++11/../../../.././libstdc++-v3/src/c++11/thread.cc:84 (libstdc++.so.6+0x0000000b045f)
Previous read of size 8 at 0x7d1c0000df90 by thread T1:
#0 main::$_0::operator()() const /home/ghost/work/test/thread_bug.cxx:46 (a.out+0x0000000ae83d)
#1 void std::_Bind_simple<main::$_0 ()>::_M_invoke<>(std::_Index_tuple<>) /usr/gcc48/include/c++/4.8.2/functional:1731 (a.out+0x0000000ae770)
#2 std::_Bind_simple<main::$_0 ()>::operator()() /usr/gcc48/include/c++/4.8.2/functional:1720 (a.out+0x0000000ae710)
#3 std::thread::_Impl<std::_Bind_simple<main::$_0 ()> >::_M_run() /usr/gcc48/include/c++/4.8.2/thread:115 (a.out+0x0000000ae6b9)
#4 execute_native_thread_routine /home/ghost/work/gcc/gcc-4.8.2/x86_64-unknown-linux-gnu/libstdc++-v3/src/c++11/../../../.././libstdc++-v3/src/c++11/thread.cc:84 (libstdc++.so.6+0x0000000b045f)
Location is heap block of size 104 at 0x7d1c0000df90 allocated by main thread:
#0 operator new(unsigned long) /home/ghost/work/llvm/projects/compiler-rt/lib/tsan/rtl/tsan_interceptors.cc:559 (a.out+0x00000004b2b9)
#1 main /home/ghost/work/test/thread_bug.cxx:43 (a.out+0x0000000a955f)
Thread T2 (tid=21274, running) created by main thread at:
#0 pthread_create /home/ghost/work/llvm/projects/compiler-rt/lib/tsan/rtl/tsan_interceptors.cc:876 (a.out+0x00000004ef6b)
#1 __gthread_create /home/ghost/work/gcc/gcc-4.8.2/x86_64-unknown-linux-gnu/libstdc++-v3/include/x86_64-unknown-linux-gnu/bits/gthr-default.h:662 (libstdc++.so.6+0x0000000b06ae)
#2 std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>) /home/ghost/work/gcc/gcc-4.8.2/x86_64-unknown-linux-gnu/libstdc++-v3/src/c++11/../../../.././libstdc++-v3/src/c++11/thread.cc:142 (libstdc++.so.6+0x0000000b06ae)
#3 main /home/ghost/work/test/thread_bug.cxx:50 (a.out+0x0000000a95ec)
Thread T1 (tid=21273, finished) created by main thread at:
#0 pthread_create /home/ghost/work/llvm/projects/compiler-rt/lib/tsan/rtl/tsan_interceptors.cc:876 (a.out+0x00000004ef6b)
#1 __gthread_create /home/ghost/work/gcc/gcc-4.8.2/x86_64-unknown-linux-gnu/libstdc++-v3/include/x86_64-unknown-linux-gnu/bits/gthr-default.h:662 (libstdc++.so.6+0x0000000b06ae)
#2 std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>) /home/ghost/work/gcc/gcc-4.8.2/x86_64-unknown-linux-gnu/libstdc++-v3/src/c++11/../../../.././libstdc++-v3/src/c++11/thread.cc:142 (libstdc++.so.6+0x0000000b06ae)
#3 main /home/ghost/work/test/thread_bug.cxx:45 (a.out+0x0000000a95b6)
SUMMARY: ThreadSanitizer: data race on vptr (ctor/dtor vs virtual call) /home/ghost/work/test/thread_bug.cxx:19 A::~A()
==================
Waiting for Done...
Destroying object...
ThreadSanitizer: reported 1 warnings
The way out
factor out the synchronization code out of dctor into a separate method and before delete the object, wait for that method to finish
1 #include <thread>
2 #include <mutex>
3 #include <condition_variable>
4 #include <iostream>
5
6 using namespace std;
7
8 class A {
9 public:
10 virtual void F() = 0;
11 void Done() {
12 F();
13 lock_guard<mutex> l{m};
14 is_done = true;
15 cv.notify_one();
16 cout << "Called Done...\n";
17 }
18
19 virtual ~A() {
20 unique_lock<mutex> l{m};
21 cout << "Waiting for Done...\n";
22 cv.wait(l, [&] {return is_done;});
23 cout << "Destroying object...\n";
24 }
25
26 private:
27 mutex m;
28 condition_variable cv;
29 bool is_done{false};
30 };
31
32 class B: public A {
33 public:
34 virtual void F() {
35 }
36
37 ~B() {
38 cout << "~B done\n";
39 }
40 };
41
42 int main() {
43 A *obj{new B};
44
45 thread t1{[=] {
46 obj->F();
47 obj->Done();
48 }};
49
50 thread t2{[=] {delete obj;}};
51 t1.join();
52 t2.join();
53
54 return 0;
55 }
56
The bug.
Thread 1 (t1):
- call B::F() # need access vptr because F() is a overridden virtual function.
- call A::Done()
Thread 2 (t2):
- call "delete obj"
- call B::~B() # need access "vptr", because A::~A() is virtual, ~B() is virtual by default,.
# For now, we are still good, no problem (all read access to vptr)
- call A::~A() # need change the "vptr" of class A to "vptr" of class B because in the destructor
# of A, it only can call virtual function of A, not the derived class. Remember virtual
# function doesn't work in ctor/dtor. This write to "vptr" will cause data race
# condition with line 46 in thread 1, which needs access the "vptr" there
- call B::~B() # need access "vptr", because A::~A() is virtual, ~B() is virtual by default,.
clang diagnosis message
ghost@ubuntu:~/work/test$ ./a.out
Called Done...
~B done
==================
WARNING: ThreadSanitizer: data race on vptr (ctor/dtor vs virtual call) (pid=21270)
Write of size 8 at 0x7d1c0000df90 by thread T2:
#0 A::~A() /home/ghost/work/test/thread_bug.cxx:19 (a.out+0x0000000b061e)
#1 B::~B() /home/ghost/work/test/thread_bug.cxx:39 (a.out+0x0000000b055e)
#2 B::~B() /home/ghost/work/test/thread_bug.cxx:37 (a.out+0x0000000b04b7)
#3 main::$_1::operator()() const /home/ghost/work/test/thread_bug.cxx:50 (a.out+0x0000000abd60)
#4 void std::_Bind_simple<main::$_1 ()>::_M_invoke<>(std::_Index_tuple<>) /usr/gcc48/include/c++/4.8.2/functional:1731 (a.out+0x0000000abc70)
#5 std::_Bind_simple<main::$_1 ()>::operator()() /usr/gcc48/include/c++/4.8.2/functional:1720 (a.out+0x0000000abc10)
#6 std::thread::_Impl<std::_Bind_simple<main::$_1 ()> >::_M_run() /usr/gcc48/include/c++/4.8.2/thread:115 (a.out+0x0000000abbb9)
#7 execute_native_thread_routine /home/ghost/work/gcc/gcc-4.8.2/x86_64-unknown-linux-gnu/libstdc++-v3/src/c++11/../../../.././libstdc++-v3/src/c++11/thread.cc:84 (libstdc++.so.6+0x0000000b045f)
Previous read of size 8 at 0x7d1c0000df90 by thread T1:
#0 main::$_0::operator()() const /home/ghost/work/test/thread_bug.cxx:46 (a.out+0x0000000ae83d)
#1 void std::_Bind_simple<main::$_0 ()>::_M_invoke<>(std::_Index_tuple<>) /usr/gcc48/include/c++/4.8.2/functional:1731 (a.out+0x0000000ae770)
#2 std::_Bind_simple<main::$_0 ()>::operator()() /usr/gcc48/include/c++/4.8.2/functional:1720 (a.out+0x0000000ae710)
#3 std::thread::_Impl<std::_Bind_simple<main::$_0 ()> >::_M_run() /usr/gcc48/include/c++/4.8.2/thread:115 (a.out+0x0000000ae6b9)
#4 execute_native_thread_routine /home/ghost/work/gcc/gcc-4.8.2/x86_64-unknown-linux-gnu/libstdc++-v3/src/c++11/../../../.././libstdc++-v3/src/c++11/thread.cc:84 (libstdc++.so.6+0x0000000b045f)
Location is heap block of size 104 at 0x7d1c0000df90 allocated by main thread:
#0 operator new(unsigned long) /home/ghost/work/llvm/projects/compiler-rt/lib/tsan/rtl/tsan_interceptors.cc:559 (a.out+0x00000004b2b9)
#1 main /home/ghost/work/test/thread_bug.cxx:43 (a.out+0x0000000a955f)
Thread T2 (tid=21274, running) created by main thread at:
#0 pthread_create /home/ghost/work/llvm/projects/compiler-rt/lib/tsan/rtl/tsan_interceptors.cc:876 (a.out+0x00000004ef6b)
#1 __gthread_create /home/ghost/work/gcc/gcc-4.8.2/x86_64-unknown-linux-gnu/libstdc++-v3/include/x86_64-unknown-linux-gnu/bits/gthr-default.h:662 (libstdc++.so.6+0x0000000b06ae)
#2 std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>) /home/ghost/work/gcc/gcc-4.8.2/x86_64-unknown-linux-gnu/libstdc++-v3/src/c++11/../../../.././libstdc++-v3/src/c++11/thread.cc:142 (libstdc++.so.6+0x0000000b06ae)
#3 main /home/ghost/work/test/thread_bug.cxx:50 (a.out+0x0000000a95ec)
Thread T1 (tid=21273, finished) created by main thread at:
#0 pthread_create /home/ghost/work/llvm/projects/compiler-rt/lib/tsan/rtl/tsan_interceptors.cc:876 (a.out+0x00000004ef6b)
#1 __gthread_create /home/ghost/work/gcc/gcc-4.8.2/x86_64-unknown-linux-gnu/libstdc++-v3/include/x86_64-unknown-linux-gnu/bits/gthr-default.h:662 (libstdc++.so.6+0x0000000b06ae)
#2 std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>) /home/ghost/work/gcc/gcc-4.8.2/x86_64-unknown-linux-gnu/libstdc++-v3/src/c++11/../../../.././libstdc++-v3/src/c++11/thread.cc:142 (libstdc++.so.6+0x0000000b06ae)
#3 main /home/ghost/work/test/thread_bug.cxx:45 (a.out+0x0000000a95b6)
SUMMARY: ThreadSanitizer: data race on vptr (ctor/dtor vs virtual call) /home/ghost/work/test/thread_bug.cxx:19 A::~A()
==================
Waiting for Done...
Destroying object...
ThreadSanitizer: reported 1 warnings
The way out
factor out the synchronization code out of dctor into a separate method and before delete the object, wait for that method to finish