防止派生析构函数中的 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,但这看起来很麻烦。
这里有两个问题,两者都不完全符合您的描述。
您的线程仅在 ~FooBase()
后停止。这意味着如果 Foo::thread_handler
曾经读取或写入它的任何成员,它们将在线程停止之前从它下面被销毁。
如果你到达析构函数的速度足够快,那么到 Foo
时 start()
可能不会在新线程上实际调用 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;
}
假设我有以下代码
#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,但这看起来很麻烦。
这里有两个问题,两者都不完全符合您的描述。
您的线程仅在
~FooBase()
后停止。这意味着如果Foo::thread_handler
曾经读取或写入它的任何成员,它们将在线程停止之前从它下面被销毁。如果你到达析构函数的速度足够快,那么到
Foo
时start()
可能不会在新线程上实际调用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;
}