【问题标题】:Destructor in a multithreaded environment?多线程环境中的析构函数?
【发布时间】:2016-10-08 05:07:18
【问题描述】:

我想知道在这样的课程中会发生什么:

class MyClass
{
private:
    std::vector<int> iVector;
    void Worker()
    {
        //Lots of stuff done with iVector
        //adding, removing elements, etc.
    }
}

假设我创建了一个使用 iVector 并对其进行修改的线程(由其中一个类成员函数调用)。除了这个 worker 之外,该类的其他成员函数都不会读取或修改这个 std::vector。

一切似乎都很好,因为工作线程是唯一使用 iVector 的线程。

但是当对象的一个​​实例被销毁时会发生什么?即使对象在工作线程完成后被销毁,iVector 的析构函数也会从主线程调用。这会导致未定义的行为吗?

谢谢!

【问题讨论】:

  • 如果对象的生命周期长于线程或对象的任何其他用途,则没有UB。
  • 顺便说一句,如果向量只被线程使用而没有其他地方使用,为什么不把它作为线程内部的局部变量呢?那么你也不必担心这样的事情。
  • 对象的生命周期不再由线程决定。但是为什么在线程B(工作线程)中修改了线程A中的vector就可以销毁呢?
  • 看来我误会了你。对象(以及向量)是否被多个线程使用?那么生命周期必须大于或等于 所有 个使用它的线程。
  • 向量被任何线程使用它的数据或方法。如果主线程运行析构函数,则该向量也被主线程使用。您必须确保工作线程和主线程之间没有数据竞争。除此之外,没有理由使用 UB。

标签: c++ multithreading


【解决方案1】:

首先,我建议在析构函数的线程上运行std::join(或您的库等效项)。这将确保线程在向量析构函数运行之前正确完成和同步。这很重要,因为向量的生命周期必须超过使用它的线程。

30.3.1.5 中的 C++11 标准以及可能更高版本的标准:

5:同步:*this表示的线程完成 与 (1.10) 对应的成功 join() 返回同步。 [ 注意: *this 上的操作不同步。 ——尾注]

现在我们必须检查 1.10 以了解更多细节,本节首先说明:

3:对象在特定点对线程 T 可见的值 是对象的初始值,分配给对象的值由 T,或由另一个线程分配给对象的值,根据 以下规则。

老实说,这很难解析,它没有具体说明提供什么样的同步连接,似乎暗示它只同步线程本身,而不是它访问的数据。因此,我会走安全路线并在加入主线程后运行atomic_thread_fence(memory_order_acquire),并在子线程完成之前运行atomic_thread_fence(memory_order_release),这应该保证在语义之前完全发生并且没有UB。

【讨论】:

  • 我已经在 MyClass 的析构函数中的线程上调用 std::join。但我不知道 std::join 会阻止调用线程丢失工作线程所做的更改:)
  • @sapito 让我快速阅读我的 C++ 标准,然后看看这是否绝对安全
  • @sapito 我已经用一些标准引号更新了我的答案,如果您不相信单独加入就足够了,还有一种实现额外安全的方法。老实说,如果 join 只同步线程终止本身,或者线程正在使用的数据,我无法从标准中解析。它也没有说明它提供了什么样的同步语义。因此为了安全起见,您可以使用栅栏路由。
  • 我认为在存在join 时不需要栅栏,因为1.10.1.9(如果A 与B 同步,则评估A 线程间发生在评估B 之前)并且join 确实同步
  • @LukaRahne 让我担心的是“注意:*this 上的操作不同步。-结束注释”部分,你知道这应该是什么意思吗?
【解决方案2】:

如果执行线程正在使用 ivector 类成员,而另一个线程使用该类成员销毁对象,则继续使用 ivector 类成员会导致未定义的行为。

即使对象在工作线程结束后被销毁, iVector 的析构函数将从主线程调用。将 这会导致未定义的行为吗?

没有。正如您所描述的,这种情况不是未定义的行为。 C++ 标准不要求对象由创建对象的同一执行线程销毁。一个执行线程可以增长,调整向量大小,然后离开或停止使用向量,然后另一个执行线程销毁整个对象。

【讨论】:

  • 即使它不是我最初的问题的一部分,也很高兴知道析构函数如何与其他线程所做的更改同步(与成员函数相反)。顺便说一句,我在说的是:线程 A 创建和销毁 std::vector(但从不修改或读取它),线程 B 修改并读取它。
  • 这个“同步”与调用析构函数的行为无关。 C++ 标准有一个完整而冗长的部分,充实了“排序”的正式规范,即当一个执行线程中的所有“更改”在另一个线程中变得可见时。销毁一个对象必须以这样一种方式排序,即所有来自其他线程的对象引用必须在销毁该对象的线程实际执行之前排序。由多线程代码自行正确排序。对象的析构函数不对此负责。
猜你喜欢
  • 2015-06-11
  • 1970-01-01
  • 1970-01-01
  • 2012-10-08
  • 2012-11-12
  • 2018-06-12
  • 2014-08-22
  • 2022-01-03
  • 2018-05-10
相关资源
最近更新 更多