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

Guarding against vtable data race in derived destructor

假设我有以下代码

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

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

我该如何防范?我通过不将 thread_handler 作为纯虚拟

来解决问题
virtual void thread_handler(){}

但是我不知道如何在 baseclass 本身中防止这种情况,我可以在 base class 中实现一个 join_thread 接口并从每个调用它派生class,但这看起来很麻烦。

这里有两个问题,两者都不完全符合您的描述。

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

  2. 如果你到达析构函数的速度足够快,那么到 Foostart() 可能不会在新线程上实际调用 thread_handler()被销毁 - 这将导致纯虚拟调用。

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

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

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

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() 事情仍然有点尴尬,但希望这个想法有意义。

你描述的是不可能的。构造对象后调用 "start"。该对象在该阶段有效。您已经避免了在构造函数中调用虚函数的常见问题,这会导致问题。任何线程调用都隐含了一个叫做 memory barrier 的东西,因此您可以相信新线程将从创建它时存在的内存视图开始。任何存在且未更改的东西都可以。
您的问题(如另一个答案中所述)是您可以在线程完成之前退出并销毁对象(及其 vtable)。
最简单的解决方法是使用 packaged task。在未来调用 "get" 可确保任务在您继续之前完成。考虑下面的代码

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