如何防范派生类析构函数中的vtable数据竞争?
咱们先拆解一下问题根源:你现在的代码里,当Foo对象被析构时,C++会先执行Foo的析构函数(哪怕你没写,也是默认的),之后才轮到FooBase的析构。但FooBase的析构才会把run_condition_设为false并等待线程结束——这时候麻烦就来了:线程可能还在疯狂调用Foo::thread_handler,可Foo的部分已经被销毁了,不仅可能访问到无效的派生类成员,更要命的是,对象的vtable已经切换到基类的了,而基类的thread_handler是纯虚函数,这时候调用直接就崩了!
下面给你几个靠谱的解决方案,按需选择:
方案1:让派生类先终止线程,再执行基类析构
核心思路是把线程停止的逻辑提前到派生类析构阶段,确保线程在派生类成员销毁前完全退出。我们可以在基类中新增一个stop方法,然后在派生类的析构函数中主动调用它:
#include <thread> #include <iostream> #include <atomic> struct FooBase { void start(){ run_condition_ = true; t_ = std::thread([this](){ thread_handler(); }); } // 新增线程停止方法,供派生类调用 void stop() { run_condition_ = false; if(t_.joinable()) t_.join(); } virtual ~FooBase(){ // 基类析构不再处理线程,交给派生类提前完成 } protected: virtual void thread_handler() = 0; std::atomic_bool run_condition_{false}; private: std::thread t_; }; struct Foo : FooBase { ~Foo() { stop(); // 先终止线程,确保thread_handler已完全退出 } void thread_handler() override { while(run_condition_){ std::cout << "Foo is working...\n"; } } };
优点:实现简单,逻辑直观;缺点:依赖派生类开发者记得调用stop,如果漏写就会重现问题。可以通过把基类析构设为protected来强制派生类必须自定义析构,降低漏写风险。
方案2:使用非虚接口(NVI)模式,控制虚函数调用时机
把线程的循环逻辑放在基类中,派生类只负责实现具体的工作逻辑,这样基类可以确保循环退出后才允许析构继续。不过还是要配合派生类提前停止线程的逻辑:
#include <thread> #include <iostream> #include <atomic> struct FooBase { void start(){ run_condition_ = true; t_ = std::thread([this](){ thread_loop(); }); } void stop() { run_condition_ = false; if(t_.joinable()) t_.join(); } virtual ~FooBase(){ } protected: // 基类控制循环,不再是纯虚函数 void thread_loop() { while(run_condition_){ do_work(); // 派生类实现具体工作 } } // 派生类只需要实现这个纯虚函数 virtual void do_work() = 0; std::atomic_bool run_condition_{false}; private: std::thread t_; }; struct Foo : FooBase { ~Foo() { stop(); } void do_work() override { std::cout << "Foo is doing work...\n"; } };
优点:把线程循环的控制权收归基类,避免派生类误写循环逻辑;缺点:同样依赖派生类调用stop,且如果do_work中持有派生类成员的引用,还是要确保线程提前停止。
方案3:用shared_ptr延长对象生命周期(最安全)
让线程持有对象的std::shared_ptr,这样即使外部的智能指针被释放,对象也会等到线程结束后才会析构,从根源上避免析构时线程还在运行的问题:
#include <thread> #include <iostream> #include <atomic> #include <memory> // 继承enable_shared_from_this,让对象可以生成自身的shared_ptr struct FooBase : std::enable_shared_from_this<FooBase> { void start(){ run_condition_ = true; // 线程持有对象的shared_ptr,延长生命周期 auto self = shared_from_this(); t_ = std::thread([self](){ self->thread_handler(); }); } virtual ~FooBase(){ run_condition_ = false; if(t_.joinable()) t_.join(); } protected: virtual void thread_handler() = 0; std::atomic_bool run_condition_{false}; private: std::thread t_; }; struct Foo : FooBase { void thread_handler() override { while(run_condition_){ std::cout << "Foo is running...\n"; } } }; // 使用示例:必须用shared_ptr创建对象 int main() { auto foo = std::make_shared<Foo>(); foo->start(); // 当foo超出作用域时,对象会等到线程结束后才析构 return 0; }
优点:完全不需要派生类做额外工作,自动保障对象生命周期与线程一致;缺点:必须使用动态分配的对象(不能用栈上对象),增加了一点使用复杂度。
内容的提问来源于stack exchange,提问作者arynaq

