使用 shared_from_this 参数等待 std::future 获取 std::async 会阻止对 this 的破坏

Posted

技术标签:

【中文标题】使用 shared_from_this 参数等待 std::future 获取 std::async 会阻止对 this 的破坏【英文标题】:pending std::future get of std::async with shared_from_this argument blocks destruction of this 【发布时间】:2020-01-31 22:02:03 【问题描述】:

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

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;

;

然而,我发现了这段代码的一个不受欢迎的特性:如果将来不是get,我只是wait(例如,如果我不关心method的结果,在这种情况下是无论如何都是无效的),然后当task被删除时,实例不会被销毁。

即做task-&gt;future.get() 给出:

running
stopped
Destructor called

但是task-&gt;future.wait() 给出:

running
stopped

通过阅读What is the lifetime of the arguments of std::async? 的答案,我相信这里的问题是std::asyncthis-&gt;shared_from_this() 参数在异步的未来无效之前不会被销毁(通过get 或销毁或其他方式) .所以这个 shared_ptr 使类实例保持活动状态。


解决方案尝试 1:

start 中的行替换为:

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

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


解决方案尝试 2:

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

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

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


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

谢谢!

【问题讨论】:

重新尝试 1:“但我一直担心没有什么可以阻止它被破坏” - 你可以添加一个等待未来的析构函数吗? 一个很好的建议,但是当我尝试这个时,异步线程会在shared_from_this 调用时挂起。此外,如果我想将此逻辑用作基类的一部分,其中主要异步工作是在虚拟方法中完成的,那么在基析构函数中等待将为时已晚 - 我会确保所有派生方法都调用在他们的析构函数中等待 我认为如果你使用组合而不是继承,这个问题可能会消失 你为什么在异步调用中使用this-&gt;shared_from_this()而不是this?您不会遇到对象的生命周期问题,因为您在销毁之前调用了getwait @MikevanDyke - 我希望行为是我不能保证会调用 get/wait ......我将在原始问题中澄清这一点。谢谢 【参考方案1】:

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

解决方案 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,此解决方案也可以工作,因为destructor of a future returned by std::async blocks 直到任务终止。最后构建未来很重要,以便在所有其他成员被销毁之前将其销毁(并因此阻塞)。如果这样做风险太大,请改用解决方案 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 这是最干净的选择。

【讨论】:

【参考方案2】:

解决方案尝试 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(通过重置共享指针)——在promise 设置其值之前删除promise 的future 是安全的。不会发生死锁,因为在 promise 尝试传输值之前,future 已被破坏。

欢迎使用此解决方案或可能更简洁的替代方案的任何 cmets。尤其是如果我忽略了一些问题会很好。

【讨论】:

以上是关于使用 shared_from_this 参数等待 std::future 获取 std::async 会阻止对 this 的破坏的主要内容,如果未能解决你的问题,请参考以下文章

shared_from_this使用boost :: asio抛出bad_weak_ptr

C++ shared_from_this() 不会让 obj 被破坏

shared_from_this

为什么调用shared_from_this调用std :: terminate

shared_from_this()如何在派生类中工作,该派生类继承自从enabled_shared_from_this继承的基类

智能指针shared_ptr新特性shared_from_this及weak_ptr