thread_local переменные могут конструироваться до обычных глобальных переменных.
Это может происходить, если кто-то создаёт thread до того, как все глобальные объекты проинициализированы,
для этой нити и будет создан инстанс thread_local переменной.
Казалось бы, в здравом уме никто сам это делать не будет.
Но сторонние приложения тоже могут инджектить свои DLLки.
И это не обязательно malware, могут быть, например, и anti-malware.
Собственно, судя по дампу, там anti-malware.
Ещё одна деталь: для сторонних нитей конструктор thread_local вызывается из TLS-callback, а внутри PE Loader в ntdll все ислючения из TLS-коллбэков по-тихому ловятся.
(ловятся как SEH, т.е. если бросить С++ исключение, то С++ объект от него утечёт)
Так, легко не знать, что ситуация имеет место, пока не подебажишься.
Вывод: не пытайтесь ничего сложного делать в конструкторе thread_local переменной, если хотите работать в Windows.
Что же касается деструкторов thread_local, то с ними, думаю, всё в порядке.
Для нити main (или нити, вызвавшей exit) в силе комментарий из exit.cpp (200), снабженный даже ссылкой на стандарт:
// If this module has any dynamically initialized
// __declspec(thread) variables, then we invoke their
// destruction for the primary thread. All thread_local
// destructors are sequenced before any atexit calls or static
// object destructors (3.6.3/1)
Других нитей как бы не должно быть, и если они есть, то для них просто не будет вызван TLS-callback с DLL_THREAD_DETACH
(объект как бы утечёт вместе со всем остальным, что утекло от работающей нити).
Русский военный корабль идёт ко дну!
Re: [MSVC] Грабли: thread_local могут конструироваться до обычных глобальных
Здравствуйте, Alexander G, Вы писали:
AG>thread_local переменные могут конструироваться до обычных глобальных переменных. AG>... AG>Вывод: не пытайтесь ничего сложного делать в конструкторе thread_local переменной, если хотите работать в Windows.
У MSVC, вообще, реализация рантайма при многопоточности не радует. При работе с глобалами (thread_local можно рассматривать как своеобразный глобал), выходит, что ничего путного делать нельзя. Например, если в классе глобала есть поток, то выход из приложения гарантированно приводит к зависанию, так как такой объект будет ждать поток в деструкторе, после main (at_exit), когда рантайм захватывает мьютекс, а поток вызовет _endthreadex который тоже будет пытаться захватить этот же мьютекс.
Интересно, под линуксом такое же поведение, или это особенности реализации MS.
Re[2]: [MSVC] Грабли: thread_local могут конструироваться до обычных глобальных
Здравствуйте, Videoman, Вы писали:
V>Например, если в классе глобала есть поток, то выход из приложения гарантированно приводит к зависанию, так как такой объект будет ждать поток в деструкторе, после main (at_exit), когда рантайм захватывает мьютекс, а поток вызовет _endthreadex который тоже будет пытаться захватить этот же мьютекс. V>Интересно, под линуксом такое же поведение, или это особенности реализации MS.
Где это наблюдалось?
В DLL так нельзя делать, да.
В приложении — можно.
У меня всё работает в VS 2015:
Здравствуйте, Alexander G, Вы писали:
AG>Где это наблюдалось?
В VS2013 наблюдается. Там в исходника рантайма явно видно — захватывается лок, а зачем начинаю зваться деструкторы глобальных объектов. _endthreadex — пытается получить этоже лок, чтобы очистить свои.
AG>В DLL так нельзя делать, да.
Я в курсе. Но там могут быть еще пляски с attach/detach потоков.
AG>В приложении — можно.
Вы уверены??? У меня ваш код стабильно виснет.
Re[4]: [MSVC] Грабли: thread_local могут конструироваться до обычных глобальных
Здравствуйте, Alexander G, Вы писали:
AG>В VS2012 — виснет в рантайме на _lock(_EXIT_LOCK1);
Интересно, это глюк ??? Или может быть это implementation defined? С другой стороны, мне не понятно, почему нельзя было сделать нормально. Зачем вызывать все пользовательские глобальные деструкторы во всех потоках в едином локе, необходимом для собственных нужд самого рантайма?! Ну, отпускали бы его, на момент вызова всех at_exit(...).
Здравствуйте, Alexander G, Вы писали:
AG>Здравствуйте, Videoman, Вы писали:
AG>Ха. Работает в VS2015. AG>И в gcc на ideone: AG>http://ideone.com/EAFDQj
AG>В VS2012 — виснет в рантайме на _lock(_EXIT_LOCK1);
Не понял сценария зависания. Можешь по шагам расписать?
— предисловие-к-main собралось инициализировать статики
— залочило глобальный мьютекс
— в конструкторе X создали новый поток
— — в этом потоке полезли что-то инициализировать (какие-то внутренние статики — например, глобальную таблицу потоков или TLS-ов)
— — для этого надо было залочить мьютекс
— — опаньки!
Так, что ли?
Перекуём баги на фичи!
Re[6]: [MSVC] Грабли: thread_local могут конструироваться до
Здравствуйте, Кодт, Вы писали:
К>Не понял сценария зависания. Можешь по шагам расписать?
К>- предисловие-к-main собралось инициализировать статики К>- залочило глобальный мьютекс К>- в конструкторе X создали новый поток К>- — в этом потоке полезли что-то инициализировать (какие-то внутренние статики — например, глобальную таблицу потоков или TLS-ов) К>- — для этого надо было залочить мьютекс К>- — опаньки!
К>Так, что ли?
Говорю как в VS2013:
1. В main создали глобальный объект. В нем, например, создали поток и запустили.
2. Выходим из main.
3. Рантайм берет глобальный лок и в нем начинает звать деструктуры глобальных объектов через механизм at_exit
4. Дойдя до деструктора глобального объекта с потоком, мы оповещается поток, тем или иным способом о том что нужно завершится и садимся на join ("виснем").
5. Поток получает сигнал и пытается завершиться. Доходит до _endthreadex.
6. _endthreadex, как я понимаю, пытается чистить глобалные объекты потока (thread_local), но перед этим пытается захватить тотже лок, что и at_exit.
7. Результат — дедлок!
Здравствуйте, Кодт, Вы писали:
К>Не понял сценария зависания. Можешь по шагам расписать?
В VS2012 оказывается хитрый баг std::thread, воспроизводится только с std::thread.
Смысл в том, что
В основном потоке:
1. После main или из exit захватили лок и бежим по atexit функциям
2. в X::~X явно ждём дополнительный поток
В дополнительном потоке:
1. для вызова at-thread-exit изнутри std::thread через call_once создаётся мьютекс
2. внутри себя call_once создаёт свою, call_once'овскую критическую секцию
3. эта критическая секция добавляет свой DeleteCriticalSection в atexit
4. atexit хочет захватить тот самый лок, под которым мы в основом потоке
Через залипуху работает:
#include <thread>
#include <atomic>
#include <chrono>
#include <iostream>
static struct Z
{
Z()
{
std::thread([]{}).join();
}
} z; // непременно выше X, иначе вообще падаетstatic struct X
{
X(): thd([this]
{
while ( ! stop.load() )
std::this_thread::sleep_for(std::chrono::milliseconds(200));
})
{
}
~X()
{
stop.store(true);
thd.join();
std::cout << "Exited\n";
}
std::atomic_bool stop;
std::thread thd;
} x;
int main()
{
std::cout << "Exiting\n";
return 0;
}