Захват thread_local в лямбде

Захват 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, то его значение в потоке, выполняющем лямбду, будет thread_local версией потока, в котором выполняется лямбда. Но как только я беру ссылку или указатель на локальный поток, он превращается в (указатель на) экземпляр исходного потока.

Какие тут правила. Могу ли я положиться на этот анализ?

Пожалуйста, объясните, что означают «работает нормально» и «не работает», чтобы людям не приходилось щелкать и выяснять, что вы могли иметь в виду.

molbdnilo 13.06.2023 14:48

Судя по моему прочтению cppreference, это просто не разрешено: лямбда-выражение может использовать переменную без ее захвата, если переменная не является локальной переменной или имеет статическую или локальную продолжительность хранения потока (в этом случае переменная не может быть захвачена) , или источник: en.cppreference.com/w/cpp/language/lambda

NathanOliver - Is on Strike 13.06.2023 14:49

@NathanOliver-IsonStrike Я думаю, все в порядке, так как в примере 2 я вообще ничего не фиксирую (вы можете удалить & в [&]. В примерах 1 и 3 я фиксирую что-то, что не является thread_local. В примере 2 все еще есть проблема, как обсуждалось в принятом ответе.

Mike Vine 13.06.2023 15:14

@MikeVine Я имел в виду, что 2 не захватывает user, хотя вы указали &. Вместо этого у вас есть новая локальная переменная потока в вашей лямбде, которая никогда не была инициализирована.

NathanOliver - Is on Strike 13.06.2023 15:18

@NathanOliver-IsonStrike: «локальная переменная нового потока в вашей лямбде». Ну не совсем. Локальная переменная потока с новым экземпляром в std::thread, которую лямбда находит при запуске внутри std::thread. Но это не «в лямбде», поскольку та же самая лямбда может быть выполнена в основном потоке и найти локальный экземпляр старого потока.

Ben Voigt 13.06.2023 16:16
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
14
5
564
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Подобно локальным 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, то он будет инициализирован при инициализации вашего потока, а не при запуске main:

thread_local Person user{"mike"};

int main() {
    // ...

В качестве альтернативы объявите user внутри вашего лямбда-выражения.


Примечание. Нет необходимости захватывать thread_local объекты. Второй пример может быть [] { ... }.

Я предполагаю, что это выбор, сделанный разработчиками C++ - следует ли (из-за отсутствия лучшей фразы) «захватывать thread_locals по имени, а не по адресу». Я предполагаю, что статика, глобальные переменные, имена функций и т. д. Все записываются по имени, но для них это действительно не имеет значения, поскольку они не меняются в потоках. Для thread_locals выбор имеет значение, и кажется, что они рассматриваются как статические, а не локальные.

Mike Vine 13.06.2023 15:08

Я предполагаю, что не «захват по имени» фактически означает, что их вообще не нужно хранить, поэтому эта лямбда в примере 2 не имеет состояния. Думаю, теперь я понял. Не то чтобы я думаю, что это правильный выбор, но я предполагаю, что пользователь может использовать метод, который я использую в примере 1 или 3, чтобы изменить это на другой вариант, поэтому это лучший выбор, даже если он немного сбивает с толку на первый взгляд.

Mike Vine 13.06.2023 15:08

@MikeVine, если бы вы использовали метод из примера 1 или 3, тогда не было бы смысла использовать thread_local, поскольку все потоки используют один и тот же объект. Кроме того, я думаю, что такое поведение для static thread_local является серьезной проблемой в C++ и очень плохим дизайном. Вы даже не получите предупреждение, и я не смог получить какое-либо дезинфицирующее средство, чтобы поймать эту ошибку.

Jan Schultke 13.06.2023 15:14

Я не думаю, что это обязательно бесполезно - я могу вспомнить случаи, когда я хочу передать thread_local по «адресу» и позволить другим потокам использовать его. Представьте себе регистратор, который записывает в файл журнала в другом потоке, и я хотел, чтобы этот регистратор ссылался на этот экземпляр потока thread_local. Это могло бы (и будет) работать через пример 1 или 3. Это была бы ниша.

Mike Vine 13.06.2023 15:18

Почему бы просто не захватить tread_id по значению? Есть ли даже гарантия того, что протектор, начавший ведение журнала, все еще «существует» к тому времени, когда ведение журнала обрабатывается в фоновом потоке. Я много занимался программированием многопоточности (около 25 лет), и мне действительно нужен был thread_local только один или два раза (так что это не первое решение, которое я ищу).

Pepijn Kramer 13.06.2023 15:24

Вот попытка форсировать проявление УБ: godbolt.org/z/zvMacxWca

Marek R 13.06.2023 15:24

Я думаю, что есть аспект, который игнорируется — см. godbolt.org/z/eTofxeYz6. Здесь нет UB, но вы все равно получаете тот же образец другого поведения. Я думаю, что в данном случае поведение правильное и ожидаемое; 1 и 3 захватывают значение из основного потока, а 2 независимо получают доступ к значению из нового потока.

Miral 14.06.2023 09:41

@Miral да, такое поведение правильное и ожидаемое. Если вы посмотрите на мой ответ, в частности на раздел «Ваши три случая объяснены», вы подтвердите, что вывод отличается, потому что используется объект user основного потока.

Jan Schultke 14.06.2023 10:27

[&] — очень опасная вещь, когда ваша лямбда выполняется вне непосредственного контекста. Там это очень полезно, иначе это плохой план.

В этом случае вас укусит тот факт, что [&] не захватывает глобальные, локальные статические или локальные переменные потока. Таким образом, использование [] в этом случае будет вести себя так же.

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_localmain::user. Поскольку поток никогда не проходил строку инициализации этой переменной, вы только что выполнили UB.

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

здесь вы явно создаете новую ссылочную переменную user при создании лямбды.

Основное правило: **никогда не используйте [&] при создании лямбды для передачи std::thread.

Это уместное использование [&]:

foreach_chicken( [&](chicken& c){ /* process chicken */ } );

Ожидается, что лямбда будет существовать в текущей области видимости и будет выполняться локально. [&] безопасно.

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 преобразуется в вспомогательную функцию и может выполняться более одного раза в локальной функции.

Но если лямбда не запускается локально или может жить за пределами текущей области видимости, [&] — это токсичная опция, которая приводит к неожиданностям и ошибкам почти во всех случаях, когда я видел ее использование.

Другие вопросы по теме