挂起的 std::将来获取 std::异步与shared_from_this参数阻止破坏它

pending std::future get of std::async with shared_from_this argument blocks destruction of this

本文关键字:std 参数 this shared 将来 获取 异步 挂起 from      更新时间:2023-10-16

我想创建一个类来表示一个可以异步启动的任务,并将连续运行(有效地在分离的线程中(,直到收到停止信号。为了这个问题,用法如下所示:

auto task = std::make_shared<Task>();
task->start(); // starts the task running asynchronously
... after some time passes ...
task->stop(); // signals to stop the task
task->future.get(); // waits for task to stop running and return its result

但是,这门Task课的一个关键特点是,我不能保证未来会被等待/得到......即在共享指针被销毁之前,最后一行可能不会被调用。

我写的类的精简玩具版本如下(请忽略所有内容都是公开的,这只是为了这个例子的简单

(:
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
~MyClass() { std::cout << "Destructor called" << std::endl; }
void start() {
future = std::async(std::launch::async, &MyClass::method, this->shared_from_this());
}
void stop() { m_stop = true; }
void method() {
std::cout << "running" << std::endl;
do {
std::this_thread::sleep_for(std::chrono::seconds(1));
} while(m_stop == false);
std::cout << "stopped" << std::endl;
return;
}
std::future<void> future;
std::atomic<bool> m_stop = false;
};

但是,我发现了这段代码的一个不良功能:如果我只是wait而不是get未来(例如,如果我不关心method的结果,在这种情况下无论如何都是空白(,那么当删除task时,实例不会被销毁。

即做task->future.get()会得到:

running
stopped
Destructor called

task->future.wait()给出了:

running
stopped

从阅读答案到 std::async 的参数的生命周期是什么?我相信这里的问题是this->shared_from_this()的论点,在异步的未来变得无效(通过get或破坏或其他方式(之前,std::async不会被销毁。因此,此shared_ptr使类实例保持活动状态。


解决方案尝试 1:

start中的行替换为:

future  = std::async(std::launch::async, [this]() {
return this->shared_from_this()->method();
});

这可确保它创建shared_ptr在方法完成时被销毁,但我一直担心在它被 lambda 捕获(发生在这一行,对吗?(和 lambda 在新线程中执行 lambda 的时间之间没有什么可以this阻止它被销毁。这是真的可能吗?


解决方案尝试 2:

为了保护在 lambda 函数运行之前销毁this(task(,我添加了另一个成员变量std::shared_ptr<MyClass> myself然后我的启动方法可以如下所示:

myself = this->shared_from_this();
future  = std::async(std::launch::async, [this]() {
auto my_ptr = std::move(this->myself);
return myself->method();
});

这里的想法是,myself将确保如果我删除taskshared_ptr,我不会破坏类。然后在 lambda 内部,shared_ptr 被转移到局部my_ptr变量,该变量在退出时被销毁。


此解决方案是否存在问题,或者我是否忽略了实现我所追求的排序功能的更简洁的方法?

谢谢!

我在某些情况下发现的解决方案尝试 2 会生成死锁异常。这似乎来自异步线程同时试图破坏未来(通过销毁类的实例(,同时也试图设置未来的值。


解决方案尝试 3- 到目前为止,这似乎通过了我的所有测试:

myself = this->shared_from_this();
std::promise<void> p;
future = p.get_future();
std::thread([this](std::promise<void>&& p) {
p.set_value_at_thread_exit( myself->method() );
myself.reset();
}, std::move(p)).detach();

这里的逻辑是,一旦方法调用完成,销毁myself(通过重置共享指针(是安全的 - 在承诺设置其值之前删除承诺的未来是安全的。不会发生死锁,因为在承诺尝试转移价值之前,未来就被破坏了。

欢迎对此解决方案或可能更整洁的替代方案提出任何评论。特别是很高兴知道是否有我忽略的问题。

我会建议以下解决方案之一:

解决方案 1,将std::asyncthis一起使用,而不是shared_from_this

class MyClass /*: public std::enable_shared_from_this<MyClass> not needed here */ {
public:
~MyClass() { std::cout << "Destructor called" << std::endl; }
void start() {
future = std::async(std::launch::async, &MyClass::method, this);
}
void stop() { m_stop = true; }
void method() {
std::cout << "running" << std::endl;
do {
std::this_thread::sleep_for(std::chrono::seconds(1));
} while(m_stop == false);
std::cout << "stopped" << std::endl;
return;
}
std::atomic<bool> m_stop = false;
std::future<void> future; // IMPORTANT: future constructed last, destroyed first
};

即使不调用waitget将来,此解决方案也会起作用,因为未来的析构函数由std::async块返回,直到任务终止。重要的是最后构建未来,以便在所有其他成员被摧毁之前将其销毁(从而破坏块(。如果风险太大,请改用解决方案 3。


解决方案 2,像以前一样使用分离的线程:

void start() {
std::promise<void> p;
future = p.get_future();
std::thread(
[m = this->shared_from_this()](std::promise<void>&& p) {
m->method();
p.set_value();
},
std::move(p))
.detach();
}

此解决方案的一个缺点是:如果有许多MyClass实例,则会创建大量线程,可能会导致争用。因此,更好的选择是使用线程池,而不是每个对象的单个线程。


解决方案 3,将可执行文件与任务类分开,例如:

class ExeClass {
public:
~ExeClass() { std::cout << "Destructor of ExeClass" << std::endl; }
void method() {
std::cout << "running" << std::endl;
do {
std::this_thread::sleep_for(std::chrono::seconds(1));
} while (m_stop == false);
std::cout << "stopped" << std::endl;
return;
}
std::atomic<bool> m_stop = false;
};
class MyClass {
public:
~MyClass() { std::cout << "Destructor of MyClass" << std::endl; }
void start() {
future = std::async(std::launch::async, &ExeClass::method, exe);
}
void stop() { exe->m_stop = true; }
std::shared_ptr<ExeClass> exe = std::make_shared<ExeClass>();
std::future<void> future;
};

与解决方案 1 一样,当未来被摧毁时,这将阻止,但是您不需要照顾构建和破坏的顺序。IMO 这是最干净的选择。