c++ 在lambda中捕获`thread_local`

egdjgwm8  于 2023-06-25  发布在  其他
关注(0)|答案(2)|浏览(234)

在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版本。但是,一旦我引用或指针到线程本地,它就会变成(指向)* 原始 * 线程示例。
这里的规矩是什么。我能相信这个分析吗?

oprakyz7

oprakyz71#

与局部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,这与第一种情况相同。

可能的修复

如果你在main之外声明user,那么它将在你的线程初始化时被初始化,而不是在main运行时被初始化:

thread_local Person user{"mike"};

int main() {
    // ...

或者,在lambda表达式中声明user

  • 注意:不需要捕获thread_local对象。第二个例子可以是[] { ... }
czfnxgou

czfnxgou2#

[&]是一个非常危险的事情,当你的lambda在直接上下文之外执行时。在那里非常有用--否则,这是一个糟糕的计划。
在这种情况下,你会被[&] * 不 * 捕获全局、局部静态或线程局部变量这一事实所困扰。所以在这种情况下使用[]的行为是一样的。

thread_local Person user{"mike"};
Person& referenceToUser = user;

std::thread([&]() {std::cout << "Hello " << referenceToUser.name << std::endl;}).join();

这通过引用捕获referenceToUserreferenceToUser反过来引用主线程中的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操作被重构到一个helper中,并且可能在本地函数中运行不止一次。
但是如果lambda不是在本地运行的,或者可能超出当前的作用域,那么[&]就是一个有毒的选项,在我见过的几乎每一个使用它的情况下都会导致意外和错误。

相关问题