在 lambda 中捕获`thread_local`

问题描述 投票:0回答:2

在 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线程实例的指针。

这里有什么规则。我可以相信这个分析吗?

c++ c++11 language-lawyer stdthread thread-local-storage
2个回答
15
投票

与本地

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
对象。第二个例子可能是
[] { ... }


1
投票
当 lambda 在直接上下文之外执行时,

[&]
是一件非常危险的事情。它在那里非常有用——否则,这是一个糟糕的计划。

在这种情况下,您会被以下事实所困扰:

[&]
确实捕获全局、局部静态或线程局部变量。因此,在这种情况下使用
[]
的行为是相同的。

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 没有在本地运行或者可能超出当前范围,那么

[&]
是一个有毒的选项,在我见过的几乎所有使用情况下都会导致意外和错误。

© www.soinside.com 2019 - 2024. All rights reserved.