可以安全地使用在线程中较早构建的对象

Safe to use object constructed earlier in a thread

假设我的代码如下:

struct Foo {
  Foo() : x(10) {}
  int x_;
}

void WillRunInThread(const Foo* f) {
  cout << "f.x_ is: " << f->x_ << endl;
}

struct Baz {
  Baz() : foo_(), t_(&WillRunInThread, &foo_) {}
  Foo foo_;
  std::thread t_;
}

int main(int argc, char** argv) {
  Baz b;
  b.t_.join();
}

我能保证 WillRunInThread 会打印 10 吗?我看到 表明这是安全的。但是,我不确定情况是否如此。具体来说,我认为存在以下两种可能性,这两种可能性都会产生问题:

  1. Foo.x_ 可以在 t_ 启动时存储在寄存器中。
  2. 编译器可以重新排序一些指令。虽然 C++ 保证 foo_ 出现 t_ 之前构造,但仅在同一线程中。例如,编译器可以自由地重新排序 foo_.x_ 的初始化,直到 t_.
  3. 之后

我没有看到任何迹象表明 std::thread 的构造函数充当任何类型的内存栅栏来防止上述两个问题。但是,我经常看到类似上面的代码,这让我觉得我遗漏了一些东西。如有任何澄清,我们将不胜感激。

从“这个变量可能存储在寄存器中”或“编译器可能想要重新排序这些指令”等实现细节的角度来思考是错误的。坚持标准告诉你的。

线程启动时有一个同步点(C++17 [thread.thread.constr]/6):

The completion of the invocation of the constructor synchronizes with the beginning of the invocation of the copy of f.

“A 与 B 同步”意味着所有在 A 之前(在 A 的线程中)完成的副作用将对在 B 之后运行的所有代码(在 B 的线程中)可见。参见 [intro.races]/9-12。

特别地,如果线程1使用std::thread构造函数启动一个线程,而线程2是启动的线程并且它的初始函数是f,那么f中的任何代码],或在从 f 调用的函数内部,将看到在执行 std::thread 构造函数之前由线程 1 引起的所有副作用。

由于 foo_.x_ 的初始化顺序在 t_ 的初始化之前,因此新线程将读取值 10。

你写的代码肯定是安全的。 (忽略错误:缺少 ;s,x 而不是 x_)。这是因为 foo_ 实际上被复制到 thread 构造函数 中。加Foo(const Foo&) = delete;就可以看到了,会编译失败

https://en.cppreference.com/w/cpp/thread/thread/thread
The arguments to the thread function are moved or copied by value.

现在,为了理解您的问题的精神,让我们将线程初始化更改为

  Baz() : foo_(), t_(&WillRunInThread, std::ref(foo_)) {}

仍然安全,因为:

The completion of the invocation of the constructor synchronizes-with (as defined in std::memory_order) the beginning of the invocation of the copy of f on the new thread of execution.