Захват 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 версией потока, в котором выполняется лямбда. Но как только я беру ссылку или указатель на локальный поток, он превращается в (указатель на) экземпляр исходного потока.
Какие тут правила. Могу ли я положиться на этот анализ?
Судя по моему прочтению cppreference, это просто не разрешено: лямбда-выражение может использовать переменную без ее захвата, если переменная не является локальной переменной или имеет статическую или локальную продолжительность хранения потока (в этом случае переменная не может быть захвачена) , или источник: en.cppreference.com/w/cpp/language/lambda
@NathanOliver-IsonStrike Я думаю, все в порядке, так как в примере 2 я вообще ничего не фиксирую (вы можете удалить & в [&]. В примерах 1 и 3 я фиксирую что-то, что не является thread_local. В примере 2 все еще есть проблема, как обсуждалось в принятом ответе.
@MikeVine Я имел в виду, что 2 не захватывает user, хотя вы указали &. Вместо этого у вас есть новая локальная переменная потока в вашей лямбде, которая никогда не была инициализирована.
@NathanOliver-IsonStrike: «локальная переменная нового потока в вашей лямбде». Ну не совсем. Локальная переменная потока с новым экземпляром в std::thread, которую лямбда находит при запуске внутри std::thread. Но это не «в лямбде», поскольку та же самая лямбда может быть выполнена в основном потоке и найти локальный экземпляр старого потока.





Подобно локальным 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 выбор имеет значение, и кажется, что они рассматриваются как статические, а не локальные.
Я предполагаю, что не «захват по имени» фактически означает, что их вообще не нужно хранить, поэтому эта лямбда в примере 2 не имеет состояния. Думаю, теперь я понял. Не то чтобы я думаю, что это правильный выбор, но я предполагаю, что пользователь может использовать метод, который я использую в примере 1 или 3, чтобы изменить это на другой вариант, поэтому это лучший выбор, даже если он немного сбивает с толку на первый взгляд.
@MikeVine, если бы вы использовали метод из примера 1 или 3, тогда не было бы смысла использовать thread_local, поскольку все потоки используют один и тот же объект. Кроме того, я думаю, что такое поведение для static thread_local является серьезной проблемой в C++ и очень плохим дизайном. Вы даже не получите предупреждение, и я не смог получить какое-либо дезинфицирующее средство, чтобы поймать эту ошибку.
Я не думаю, что это обязательно бесполезно - я могу вспомнить случаи, когда я хочу передать thread_local по «адресу» и позволить другим потокам использовать его. Представьте себе регистратор, который записывает в файл журнала в другом потоке, и я хотел, чтобы этот регистратор ссылался на этот экземпляр потока thread_local. Это могло бы (и будет) работать через пример 1 или 3. Это была бы ниша.
Почему бы просто не захватить tread_id по значению? Есть ли даже гарантия того, что протектор, начавший ведение журнала, все еще «существует» к тому времени, когда ведение журнала обрабатывается в фоновом потоке. Я много занимался программированием многопоточности (около 25 лет), и мне действительно нужен был thread_local только один или два раза (так что это не первое решение, которое я ищу).
Вот попытка форсировать проявление УБ: godbolt.org/z/zvMacxWca
Я думаю, что есть аспект, который игнорируется — см. godbolt.org/z/eTofxeYz6. Здесь нет UB, но вы все равно получаете тот же образец другого поведения. Я думаю, что в данном случае поведение правильное и ожидаемое; 1 и 3 захватывают значение из основного потока, а 2 независимо получают доступ к значению из нового потока.
@Miral да, такое поведение правильное и ожидаемое. Если вы посмотрите на мой ответ, в частности на раздел «Ваши три случая объяснены», вы подтвердите, что вывод отличается, потому что используется объект user основного потока.
[&] — очень опасная вещь, когда ваша лямбда выполняется вне непосредственного контекста. Там это очень полезно, иначе это плохой план.
В этом случае вас укусит тот факт, что [&] не захватывает глобальные, локальные статические или локальные переменные потока. Таким образом, использование [] в этом случае будет вести себя так же.
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 преобразуется в вспомогательную функцию и может выполняться более одного раза в локальной функции.
Но если лямбда не запускается локально или может жить за пределами текущей области видимости, [&] — это токсичная опция, которая приводит к неожиданностям и ошибкам почти во всех случаях, когда я видел ее использование.
Пожалуйста, объясните, что означают «работает нормально» и «не работает», чтобы людям не приходилось щелкать и выяснять, что вы могли иметь в виду.