在 lambda 中捕获 thread_local:
#include <iostream>
#include <thread>
#include <string>
struct Person
{
std::string name;
};
int main()
{
thread_local Person user{"mike"};
Person& referenceToUser = user;
// Works fine - Prints "Hello mike"
std::thread([&]() {std::cout << "Hello " << referenceToUser.name << std::endl;}).join();
// Doesn't work - Prints "Hello"
std::thread([&]() {std::cout << "Hello " << user.name << std::endl;}).join();
// Works fine - Prints "Hello mike"
std::thread([&user=user]() {std::cout << "Hello " << user.name << std::endl;}).join();
}
https://godbolt.org/z/zeocG5ohb
看起来如果我使用
thread_local
的原始名称,那么执行 lambda 的线程上的值就是运行 lambda 的线程的 thread_local
版本。但是,一旦我获取本地线程的引用或指针,它就会变成(指向)originating线程实例的指针。
这里有什么规则。我可以相信这个分析吗?
与本地
static
对象类似,本地 thread_local
(隐式 static thread_local
)对象在控制第一次通过其声明时被初始化。
您创建的线程永远不会执行
main
,只有主线程执行,因此您在额外线程上的生命周期开始之前访问 user
。
std::thread([&]() {std::cout << "Hello " << referenceToUser.name << std::endl;}).join();
我们正在捕获
referenceToUser
,它指的是主线程上的 user
。这没关系。
std::thread([&]() {std::cout << "Hello " << user.name << std::endl;}).join();
我们正在额外线程的生命周期开始之前访问
user
。这是未定义的行为。
std::thread([&user=user]() {std::cout << "Hello " << user.name << std::endl;}).join();
我们再次从主线程引用
user
,这与第一种情况相同。
如果您在
user
之外声明 main
,则 user
将在线程启动时初始化,而不是在 main
运行时初始化:
thread_local Person user{"mike"};
int main() {
// ...
或者,在 lambda 表达式内声明
user
。
注意:无需捕获
thread_local
对象。第二个例子可能是[] { ... }
。
[&]
是一件非常危险的事情。它在那里非常有用——否则,这是一个糟糕的计划。
在这种情况下,您会被以下事实所困扰:
[&]
确实不捕获全局、局部静态或线程局部变量。因此,在这种情况下使用 []
的行为是相同的。
thread_local Person user{"mike"};
Person& referenceToUser = user;
std::thread([&]() {std::cout << "Hello " << referenceToUser.name << std::endl;}).join();
这通过引用捕获了
referenceToUser
。 referenceToUser
又指主线程中的thread_local
变量。
std::thread([&]() {std::cout << "Hello " << user.name << std::endl;}).join();
这与
相同std::thread([]() {std::cout << "Hello " << user.name << std::endl;}).join();
此处使用
[&]
让您相信它正在通过引用捕获 user
。因此使用了 thread_local
变量 main::user
。由于线程从未通过该变量的初始化行,因此您刚刚完成了 UB。
std::thread([&user=user]() {std::cout << "Hello " << user.name << std::endl;}).join();
在这里,您在 lambda 创建时显式创建了一个新的引用变量
user
。
基本规则是**在创建 lambda 传递给
[&]
时切勿使用 std::thread
。
这是
[&]
的正确使用:
foreach_chicken( [&](chicken& c){ /* process chicken */ } );
lambda 预计存在于当前范围内,并将在本地执行。
[&]
很安全。
auto pop = [&]()->std::optional<int>{
if (queue.empty()) return std::nullopt;
auto x = std::move(queue.front());
queue.pop_front();
return x;
};
while (auto x = pop()) {
}
这是有效使用
[&]
的另一个示例,因为这个 pop
操作正在被重构为帮助程序,并且可能在本地函数中运行多次。
但是,如果 lambda 没有在本地运行或者可能超出当前范围,那么
[&]
是一个有毒的选项,在我见过的几乎所有使用情况下都会导致意外和错误。