在派生的析构函数中防止vtable数据竞争

ary*_*naq 5 c++ thread-safety vtable c++11

假设我有以下代码

#include <thread>
#include <iostream>
#include <atomic>

struct FooBase {
    void start(){
        run_condition_ = true;
        t_ = std::thread([this](){
            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 derived thread.." << std::endl;
        }
    }
};


int main(){
    Foo f;
    f.start();

    getchar();

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

在这里,我认为因为派生类的析构函数在vtable查找发生在基类Foo之前被调用,如果线程在完成析构函数时尚未加入(仍在运行).由于是纯粹的虚拟我基本上保证了sigabort.FooBasethread_handlerFooFooBase::thread_handler

我该如何防范这个?我没有thread_handler像纯虚拟那样破解我的方式

virtual void thread_handler(){}
Run Code Online (Sandbox Code Playgroud)

但我很遗憾在基类本身如何防范这种情况,我可以在基类中实现一个join_thread接口并从每个派生类中调用它,但这看起来很麻烦.

Bar*_*rry 4

这里有两个问题,两者都与您所描述的不完全匹配。

  1. 你的线程只会在 中停止~FooBase()。这意味着,如果Foo::thread_handler对其任何成员进行读取或写入,它们将在线程停止之前从其下方被销毁。

  2. 如果您足够快地到达析构函数,则在被销毁时可能start()不会在新线程上实际调用- 这将导致纯虚拟调用。thread_handler()Foo

无论哪种方式,您都需要确保在Foo销毁时,所有相关的事情thread_handler都已完成。这意味着每个派生类都FooBase必须在其析构函数中具有:

run_condition_ = false;
if (t_.joinable()) {
    t_join();
}
Run Code Online (Sandbox Code Playgroud)

抛开这直接不起作用,因为t_private您可以将其包装到protected stop()),如果所有派生类都需要做一些特殊的事情才能工作,那么这是一个尴尬的设计。您可以放入FooBase它自己的类中,该类只接受任意可调用对象作为参数:

class joining_thread {
public:
    joining_thread() = default;
    ~joining_thread() { stop(); }

    bool running() const { return run_condition_.load(); }

    template <typename... Args>
    void start(Args&&... args) {
        run_condition_ = true;
        t_ = std::thread(std::forward<Args>(args)...);
    }

    void stop() {
        run_condition_ = false;
        if (t_.joinable()) t_.join();
    }
private:
    std::thread t_;
    std::atomic_bool run_condition_{false};
};
Run Code Online (Sandbox Code Playgroud)

然后你就Foo可以成为会员了:

class Foo {
public:
    void start() {
        t_.start([this]{
            while (t_.running()) { ... }
        });
    }

private:
    // just make me the last member, so it's destroyed first
    joining_thread t_;
};
Run Code Online (Sandbox Code Playgroud)

整个事情仍然有点尴尬running(),但希望这个想法是有意义的。