【问题标题】:Is it undefined behavior to run a member function in a separate thread, in parallel to the type's constructor?与类型的构造函数并行在单独的线程中运行成员函数是否是未定义的行为?
【发布时间】:2020-06-27 23:28:44
【问题描述】:

这是你不应该做的场景,但https://timsong-cpp.github.io/cppwp/class.cdtor#4 声明:

成员函数,包括虚函数 ([class.virtual]),可以在构造或销毁 ([class.base.init]) 期间调用。

如果函数被并行调用,这是否成立?也就是说,忽略竞争条件,如果 A 处于构造中间,并且在调用构造函数之后的某个时间点(例如在构造期间)调用了 frobme,那仍然是定义的行为吗?

#include <thread>

struct A {
    void frobme() {}
};

int main() {
    char mem[sizeof(A)];

    auto t1 = std::thread([mem]() mutable { new(mem) A; });
    auto t2 = std::thread([mem]() mutable { reinterpret_cast<A*>(mem)->frobme(); });

    t1.join();
    t2.join();
}

作为一个单独的场景,也有人向我指出,A 的构造函数可以创建多个线程,这些线程可能会在 A 完成构造之前调用成员函数函数,但是这些操作的顺序将更易于分析(您知道在构造函数中生成线程之前不会发生竞争)。

【问题讨论】:

  • “忽略竞争条件”?竞争条件是问题的核心。如果t2首先运行,则没有对象可以调用成员函数
  • 是的,这是一个巨大的危险信号。我想我在想,如果有一些神奇的同步原语,你可以确保 frobme 在构造函数的第一条指令之后执行,那会被认为是未定义的行为。这是从静态分析的角度来的。
  • 可能在构造函数初始化列表中锁定/释放互斥锁的一些技巧可以确保构造开始但在调用成员函数时尚未完成
  • 这段代码还有一个问题——reinterpret_cast&lt;A*&gt;(mem) 不能保证指向由placement-new 创建的对象。您应该使用placement-new“返回”的指针。此外,mem 可能与 A 不一致,但我想您为了简洁而省略了该细节,因此我们可以忽略这一点。
  • @M.M 在平面架构上,指针不包含地址以外的任何信息。所以reinterpret_cast&lt;A*&gt;(mem) 可以,即使标准委员会喜欢假装它不是。这是一个很难解决的重大性病问题。

标签: c++ constructor language-lawyer race-condition object-lifetime


【解决方案1】:

这里有两个问题:您的具体代码和一般问题。

在您的特定代码中,即使在最好的情况下(t2t1 之后执行),由于创建和使用之间缺乏同步,您也会遇到数据竞争。这使您的代码无论执行顺序如何都是 UB。

在一般问题中,我们假设一个类型的构造函数将this 指针传递给其他线程,然后该线程调用它上面的函数,并且传递本身是正确同步的。调用成员函数的其他线程是否会被视为数据竞争?

好吧,如果另一个线程调用一个函数来读取成员值或在移交点之后由构造函数写入的其他数据,或者如果构造函数访问成员或其他写入的数据,那肯定会发生数据竞争通过被调用的成员函数。也就是说,如果同时执行的代码之间没有数据竞争。

假设这两种情况都不是,那么一切都应该没问题(大多数情况下。可以定义A,使您的reinterpret_cast 不会返回指向您创建的A 的可用指针在那个存储中;you'd need to launder it)。可以访问正在构造/销毁的对象,但只能访问 in certain ways. 坚持这些方式,你应该没问题...有一个可能的问题。

标准中没有关于对象初始化完成时的数据竞争,只有内存位置冲突。一旦对象完全构造,virtual 函数的行为可能会改变,基于更改 vtable 指针等,如果动态类型是派生自给其他线程的类的类。我不相信在对象模型部分对此有明确的说明。

另外,请注意 C++20 为 class.cdtor 添加了一个特殊规则:

在对象的构造过程中,如果对象或其任何子对象的值通过不是直接或间接从构造函数的this指针获得的glvalue访问,则该对象或子对象的值如此获得的是未指定的。

【讨论】:

  • 我认为@Ben Voight 关于glvalues 的回答在这种情况下是关键。对于非静态成员函数,在生命周期开始之前使用对象的任何 glvalue 是 UB。在构造函数/初始化器列表中使用this 是可以的,因为它是prvalue。
  • @MikeLui: this 是纯右值,是的,但它是一个指针的值。取消引用指针会创建...一个glvalue。正是通过该glvalue,您可以访问该对象。这就是为什么他引用的部分对于正在构造/销毁的对象有一个特定的例外
  • @MikeLui 您的评论不正确——请参阅我在 Ben 的回答下的第一条评论。对于正在构建的对象,其行为由 class.cdtor 涵盖,而不是由 basic.life/6 下的项目符号所涵盖,后者由“否则”一词引入
  • @M.M:在与placement-new同时执行的代码中(就像这个问题一样),不知道对象是预构造的、正在构造的还是完全构造的。
  • @BenVoigt 我不想在此处重复您的回答下的讨论。在 Nicol 的回答中,它解决了一个假设代码,其中已知对象正在构建中(请参阅此答案的第三段)
【解决方案2】:

除了竞争条件(您可能使用互斥锁或类似条件进行管理)之外,您还受到生命周期尚未开始的对象的通常限制,即:

在对象的生命周期开始之前但在对象将占用的存储空间已分配之后,或者在对象的生命周期结束之后且在对象占用的存储空间被重用或释放之前,任何表示可以使用对象将要或曾经位于的存储位置的地址,但只能以有限的方式使用。

请参阅[basic.life] 了解允许和不允许的操作的完整列表。

特别是,其中一个限制是

如果出现以下情况,则程序具有未定义的行为:

...

  • glvalue 用于调用对象的非静态成员函数

这显然禁止你的例子。

还有[class.cdtor] 说:

对于具有非平凡构造函数的对象,在构造函数开始执行之前引用对象的任何非静态成员或基类会导致未定义的行为

即使您确实同步到在构造开始后触发的某些事件,this rule 也会禁止该代码:

在对象的构造过程中,如果对象或其任何子对象的值通过不是从构造函数的this指针直接或间接获得的glvalue访问,则该对象或子对象的值如此获得的是未指定的

【讨论】:

  • “对于正在构造或销毁的对象,请参阅 [class.cdtor]”。部分?这在这里很重要,因为这正是正在讨论的情况。
  • 在你第一次引用之后,它会立即显示“对于正在构建或销毁的对象,请参阅 [class.cdtor]。否则,”。您的第二个引用是“否则”的一部分,因此不适用,因为我们正在谈论正在建设的对象的情况。实际上,您在此处列出的情况意味着没有构造函数可以调用任何成员函数
  • @M.M:构造和成员函数调用之间没有happens-before关系,所以不能保证对象在构造中。
  • @BenVoigt:然后是 UB,因为数据竞争,所以这部分甚至不适用。
  • @BenVoigt 如果构造函数已启动但尚未完成,则对象正在构建中。这与比赛条件密不可分。如果你说“除了比赛条件”,那么下面的文字只能适用于比赛发生而导致构造函数开始的情况。 IOW 我同意引用的文本阻止在构造函数启动之前调用成员函数,但指出竞争条件已经涵盖了这一点
猜你喜欢
  • 2017-05-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多