可以安全地使用在线程中较早构建的对象
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 吗?我看到 表明这是安全的。但是,我不确定情况是否如此。具体来说,我认为存在以下两种可能性,这两种可能性都会产生问题:
Foo.x_
可以在 t_
启动时存储在寄存器中。
- 编译器可以重新排序一些指令。虽然 C++ 保证
foo_
将 出现 在 t_
之前构造,但仅在同一线程中。例如,编译器可以自由地重新排序 foo_.x_
的初始化,直到 t_
. 之后
我没有看到任何迹象表明 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.
假设我的代码如下:
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 吗?我看到
Foo.x_
可以在t_
启动时存储在寄存器中。- 编译器可以重新排序一些指令。虽然 C++ 保证
foo_
将 出现 在t_
之前构造,但仅在同一线程中。例如,编译器可以自由地重新排序foo_.x_
的初始化,直到t_
. 之后
我没有看到任何迹象表明 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.