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

时间:2018-04-12 22:07:44

标签: c++ c++11 thread-safety vtable

假设我有以下代码

#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;
}

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

我该如何防范这个?我没有将thread_handler作为纯虚拟

来破解我的方式
virtual void thread_handler(){}

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

2 个答案:

答案 0 :(得分:4)

这里有两个问题,两者都不符合你所描述的内容。

  1. 您的主题仅在~FooBase()中停止。这意味着如果Foo::thread_handler读取或写入任何成员,它们将在线程停止之前从其下面被销毁。

  2. 你足够快地到达析构函数,start()thread_handler()实际上在新线程上实际调用Foo的时间{{1}被破坏 - 这将导致纯虚拟呼叫。

  3. 无论哪种方式,您都需要确保在销毁Foo时,与thread_handler相关的任何内容都已完成。这意味着来自FooBase的每个派生类必须在其析构函数中具有:

    run_condition_ = false;
    if (t_.joinable()) {
        t_join();
    }
    

    撇开这是不可行的,因为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};
    };
    

    然后你的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_;
    };
    

    对于整个running()事情,这仍然有点尴尬,但希望这个想法是有道理的。

答案 1 :(得分:1)

您描述的内容是不可能的。在构造对象后调用“start”。该对象在该阶段有效。您已经避免了在构造函数中调用虚函数的常见问题,这会导致问题。任何线程调用都隐含着一种称为memory barrier的东西,因此您可以依赖于新线程将从创建它时存在的内存视图开始的事实。任何存在的东西都没有改变,很好 您的问题(如另一个答案中所述)是您可以在线程完成之前退出并销毁对象(并且它是vtable)。
最简单的解决方法是使用packaged task。调用“获取”未来可确保在继续之前完成任务。请考虑以下代码

#include "stdafx.h"
#include <thread>
#include <iostream>
#include <atomic>
#include <future>

int main()
{
    std::atomic<bool> stop{ false };
    std::future<void> sync;
    std::packaged_task<void()> task([&stop]()
    {
        while (!stop) 
        { 
            std::cout << "Running\n"; 
        }
    });
    std::thread thread([&task]() {task();});
    getchar();
    stop = true;
    task.get_future().get();
    thread.join();
    return 0;
}