析构函数和异步任务

Posted

技术标签:

【中文标题】析构函数和异步任务【英文标题】:Destructors and asynchronous tasks 【发布时间】:2015-04-11 21:45:26 【问题描述】:

我有一个类调用异步任务,在他的构造函数中使用std::async 来加载其内容。 (我希望异步加载对象)

代码如下:

void loadObject(Object* object)

 // ... load object


Object::Object(): 

    auto future = std::async(std::launch::async, loadObject, this);

我在我的主线程上创建和删除了这些对象的几个实例,它们可以随时被删除,甚至在加载完成之前。

我想知道当对象仍在另一个线程上处理时被销毁是否危险。如果对象被破坏,我该如何停止线程?

编辑:由于bug,std::future 析构函数不会使用我正在使用的 VS2013 编译器阻止我的代码。

【问题讨论】:

更改一个对象并在另一个线程中同时访问它而不进行同步是未定义的行为。当您正在处理的对象从您的脚下被删除时,您期望会发生什么? “他们可以随时被删除,甚至在加载完成之前。”不不不不不! @LightningRacisinObrit,删除尚未加载的smth并没有错。你只需要自动加载它(顺便说一句,是你开发的现代浏览器永远不会在中间停止加载页面?) @Lol4t0:你错了。 @LightningRacisinObrit,没有。是你 【参考方案1】:

正如 MikeMB 已经提到的,您的构造函数在加载完成之前不会完成。检查此问题以了解如何克服:Can I use std::async without waiting for the future limitation?

我想知道当对象仍在另一个线程上处理时被销毁是否危险。

删除后访问对象的内存肯定是危险的,是的。行为将是未定义的。

如果对象被破坏,我如何停止线程?

我建议您首先注意的是确保对象不会在它仍然被将要使用它的东西指向时被破坏。

一种方法是使用表示已完成加载的成员标志,该标志在异步任务中更新并在析构函数中检查,并将访问与条件变量同步。这将允许析构函数阻塞,直到异步任务完成。

一旦您设法防止对象被销毁,您可以使用另一个同步成员标志来表示对象正在被销毁并跳过加载(如果已设置)。这会增加同步开销,但如果加载成本很高,这可能是值得的。

另一种避免阻塞析构函数的方法是将std::shared_ptr 传递给异步任务,并要求所有Object 实例由共享指针拥有。这种限制可能不是很理想,您需要继承 std::enable_shared_from_this 才能在构造函数中获取共享指针。

【讨论】:

【参考方案2】:

您的代码中没有任何异步发生,因为构造函数会阻塞,直到 loadObject() 返回(std::async 返回的未来的析构函数隐式连接)。

如果不是,这将取决于您如何编写代码(尤其是您的析构函数),但很可能您的代码会产生未定义的行为。

【讨论】:

我已经听说过这个析构函数加入的事情,我有疑问,但是我测试了我的程序,它似乎真的是异步的。我的对象上有一个“update()”成员,每帧都会调用它并告诉我对象是否已加载……我可以看到它在构造后的几帧中打印出它不是。 "但是我已经测试了我的程序,它似乎真的是异步的" 那你测试错了。 加上通过将 std::future 设置为我的班级我的成员(所以它不再被破坏)我得到完全相同的结果。我试着举个例子。 @user3134405:您使用的是什么编译器和标志?我相信。 Herb Sutter 提出了一项针对 c++14 更改 std::async 的行为的建议。据我所知,它被删除了,但也许有一些编译器以不符合的方式实现std::async 显然它将在下一个主要版本中修复:connect.microsoft.com/VisualStudio/feedback/details/810623【参考方案3】:

是的当对象仍在另一个线程上处理时被销毁是很危险的

您实际上可以根据需求和期望的行为实施很多策略。

我会在这里实现某种 pimpl 策略,这意味着所有实际数据都将存储在您的对象持有的指针中。您会将所有数据加载到数据指针对象并将其存储在公共对象中原子地

从技术上讲,对象应该在构造器完成时完全构造并准备好使用。在您的情况下,数据指针对象可能仍无法使用。你应该让你的班级正确处理这种状态。

所以我们开始吧:

class Object

   std::shared_ptr<Object_data> d;
   Object::Object(): 
      d(std::make_shared<Object_data>())
   
        some_futures_matser.add_future(std::async(std::launch::async, loadObject, d));

   

然后你在你的数据对象中创建原子标志,这将表明加载完成并且对象可以使用了。

class Object_data

    // ...
    std::atomic<bool> loaded false;
;

loadObject(std::shared_ptr<Object_data> d)

    /// some load code here
    d->loaded = true;

每次通过loaded 标志访问对象时(以线程安全方式),您都必须检查对象是否已构造

【讨论】:

您没有做任何事情来确保在其他地方使用对象时安全地销毁它(正如我已经说过的那样,这是不可能的)。您所做的只是添加一个 blockdefer 对象的销毁,直到它在其他地方使用。 "some_futures_matser" - 这是什么? "some_futures_matser" 只是为了不自动加入 @LightningRacisinObrit ,问题仅说明对象将在某处使用或删除,并且在数据加载过程中。因此可以在数据加载完成之前将其删除。这里怎么不安全? @Lol4t0:在问题下的 cmets 中,您声称发布此答案是为了捍卫以下声明:“删除尚未加载的 smth 并没有错” .这种说法实际上是不准确的,这个答案并不能证明不是这样。

以上是关于析构函数和异步任务的主要内容,如果未能解决你的问题,请参考以下文章

Js执行机制,同步任务异步任务

同源和异步的区别

同步任务和异步任务

C# ForEach 循环,带有异步任务和依赖的后异步任务

06 Spring 异步执行,任务调度(@Schedule、@Async)

异步/等待和任务