数据成员线程和互斥锁的初始化。错误的订单是否具有未定义的行为?

时间:2018-11-15 16:00:45

标签: c++ multithreading

我偶然发现了我认为是不知不觉射击自己脚部的一种非常简单的方法。


先介绍一下

数据成员的初始化顺序是数据成员的声明顺序。所以这是非法的:

<h1>My Title<p>My text</p></h1>

因为在struct A { std::size_t i_; std::size_t length_; A(std::size_t length) : i_{length_} // UB here. `length_` is uninitialized length_{length} {} }; 的初始化程序中使用数据成员length_时未初始化。幸运的是i_gcc对此都给出了非常好的警告。一种简单的解决方案是根据参数clang初始化每个数据成员。


现在到重点

但是当它不是立即显而易见时,该怎么办。例如。当数据成员是i_{length}

std::thread

使用数据成员初始化程序时也会出现相同的情况:

struct X
{
    std::thread thread_;
    std::mutex mutex_;

    X() : thread_{&X::worker_thread, this}
    {}

    auto worker_thread() -> void
    {
        // use mutex_  
        std::lock_guard lk{mutex_}; // boom?
        // ..
    }
};

这看起来很无辜,struct X { std::thread thread_{&X::worker_thread, this}; std::mutex mutex_; }; gcc均未警告这种情况。这并不奇怪,因为依赖项是隐藏的。

我想上面的情况并不罕见,因此我正在确认这确实是UB。最后声明clang数据成员,或者默认将其初始化并在以后分配。

2 个答案:

答案 0 :(得分:5)

是的,这的确是未定义的行为。实际上,您已经使示例与线程和互斥体过于复杂了。每次在初始化成员时(显式或隐式)使用 // LookAt = translation + direction mLookAt[0] = headSetX + directionX; mLookAt[1] = headSetY + directionY; mLookAt[2] = headSetZ + directionZ; 时,您都会遇到麻烦。较简单的示例:

this

从成员初始化中调用非静态成员函数总是非常危险的;从构造函数主体调用成员函数时,通常也要小心。

答案 1 :(得分:1)

有两种可能的UB:

  • 如果这里mutex_的成员函数的调用是UB。
  • 如果mutex_的初始化和对其的访问导致出现问题的数据争用。

成员函数的调用将导致UB

  1. std :: thread的构造在std :: mutex的构造之前进行排序,并且
  2. std :: mutex并非简单可构造。
  

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

     

33.3.2.2线程构造函数[...] 6.同步:构造函数的调用完成与f副本的调用开始同步。

     

33.4.3.2.3互斥锁类型应为DefaultConstructible和Destructible。

mutex_的初始化在std :: thread初始化之后进行排序(因为它们是数据成员),该初始化与线程的开头同步。并且如果std::mutex不可构造(这是未指定的)。然后,由于在构造对象之前访问对象,这将导致潜在的UB。鉴于成员函数的调用和初始化可能是并发的。

用于数据竞赛:

  

6.8.2.1如果两个表达式求值中的一个修改了内存位置(6.6.1),而另一个表达式读取或修改了相同的内存位置,则冲突。

     

6.8.2.1.20如果一个程序的执行包含两个潜在的并发冲突动作,其中至少一个不是原子动作,并且没有一个在另一个动作之前发生,则执行程序包含一个数据争用,除了信号处理程序的特殊情况外如下面所描述的。任何此类数据争用都会导致不确定的行为。

很有可能std::mutex的构造将修改某些需要由std :: mutex :: lock修改的内存位置,但是也很有可能这种修改是原子的。但是它们没有被标准指定。

结论是,我认为这种用法是否会导致不确定的行为尚无定论。