或者...如何正确结合并发、RAII 和多态性?
这是一个非常实际的问题。我们被这个组合所困扰,总结为Chandler Carruth 的可怕 bug(标记为 1:18:45)!
如果你喜欢 bug,请尝试在这里抓住谜题(改编自 Chandler 的演讲):
#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;
}
这个问题(通过
clang++ -fsanatize=thread
编译时发现)归结为虚拟表的读取(多态性)和对其上的 write(在进入 ~A 之前)之间的竞争。写入是作为销毁链的一部分完成的(因此在 A 的析构函数中不会调用 B 中的方法)。
建议的解决方法是将同步移到析构函数之外,强制类的每个客户端调用 WaitUntilDone/Join 方法。这很容易被忘记,这正是我们首先想要使用 RAII 习惯用法的原因。
因此,我的问题是: