Or... how to properly combine Concurrency, RAII and Polymorphism?
This is a very practical issue. We were bitten by this combination, summarized as Chandler Carruth's terrifying bug (at 1:18:45 mark)!
If you like bugs, try to catch the puzzle here (adapted from Chandler's talk):
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
class A {
public:
virtual void F() = 0;
void Done() {
std::lock_guard<std::mutex> l{m};
is_done = true;
cv.notify_one();
std::cout << "Called Done..." << std::endl;
}
virtual ~A() {
std::unique_lock<std::mutex> l{m};
std::cout << "Waiting for Done..." << std::endl;
cv.wait(l, [&] {return is_done;});
std::cout << "Destroying object..." << std::endl;
}
private:
std::mutex m;
std::condition_variable cv;
bool is_done{false};
};
class B: public A {
public:
virtual void F() {}
~B() {}
};
int main() {
A *obj{new B{}};
std::thread t1{[=] {
obj->F();
obj->Done();
}};
delete obj;
t1.join();
return 0;
}
The issue (spotted when compiled via clang++ -fsanatize=thread
) boils down to a race between a read of the virtual table (polymorphism) and a write on it (before entering ~A). The write being done as part of the destruction chain (so no method from B is called in A's destructor).
The recommended workaround is to move the synchronization outside the destructor, forcing each client of the class to call a WaitUntilDone/Join method. This is easy to forget and that's exactly why we wanted to use the RAII idiom in the first place.
Thus, my questions are:
- Is there a nice way to enforce synchronization in base destructor?
- Out of curiosity, why on earth the virtual table is even used from the destructor? I would have expected static binding here.