Или ... как правильно сочетать параллелизм, RAII и полиморфизм?
Это очень практичный вопрос.Мы были укушены этой комбинацией, которая описана как ужасающая ошибка Чендлера Каррута (при отметке 1:18:45)!
Если вам нравятся ошибки, попробуйте поймать загадку здесь (адаптировано из Chandler'stalk):
#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
) сводится к гонке между чтением виртуальной таблицы (полиморфизм) и записью на нее (перед входом в ~ А).Запись выполняется как часть цепочки уничтожения (поэтому в деструкторе A не вызывается метод из B).
Рекомендуемый обходной путь - переместить синхронизацию за пределы деструктора, заставляя каждого клиента класса вызыватьWaitUntilDone / метод Join.Это легко забыть, и именно поэтому мы в первую очередь хотели использовать идиому RAII.
Таким образом, мои вопросы таковы:
- Есть ли хороший способ обеспечить синхронизациюв базовом деструкторе?
- Из любопытства, почему на земле виртуальный стол даже используется из деструктора?Я бы ожидал статическое связывание здесь.