[MSVC] Грабли: thread_local могут конструироваться до обычных глобальных
От: Alexander G Украина  
Дата: 15.03.17 13:06
Оценка: 18 (1)
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 могут конструироваться до обычных глобальных
От: Videoman Россия https://hts.tv/
Дата: 15.03.17 13:49
Оценка:
Здравствуйте, Alexander G, Вы писали:

AG>thread_local переменные могут конструироваться до обычных глобальных переменных.

AG>...
AG>Вывод: не пытайтесь ничего сложного делать в конструкторе thread_local переменной, если хотите работать в Windows.

У MSVC, вообще, реализация рантайма при многопоточности не радует. При работе с глобалами (thread_local можно рассматривать как своеобразный глобал), выходит, что ничего путного делать нельзя. Например, если в классе глобала есть поток, то выход из приложения гарантированно приводит к зависанию, так как такой объект будет ждать поток в деструкторе, после main (at_exit), когда рантайм захватывает мьютекс, а поток вызовет _endthreadex который тоже будет пытаться захватить этот же мьютекс.
Интересно, под линуксом такое же поведение, или это особенности реализации MS.
Re[2]: [MSVC] Грабли: thread_local могут конструироваться до обычных глобальных
От: Alexander G Украина  
Дата: 15.03.17 15:08
Оценка:
Здравствуйте, Videoman, Вы писали:

V>Например, если в классе глобала есть поток, то выход из приложения гарантированно приводит к зависанию, так как такой объект будет ждать поток в деструкторе, после main (at_exit), когда рантайм захватывает мьютекс, а поток вызовет _endthreadex который тоже будет пытаться захватить этот же мьютекс.

V>Интересно, под линуксом такое же поведение, или это особенности реализации MS.

Где это наблюдалось?

В DLL так нельзя делать, да.
В приложении — можно.
У меня всё работает в VS 2015:
#include <thread>
#include <atomic>
#include <chrono>
#include <iostream>

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;
}
Русский военный корабль идёт ко дну!
Re[3]: [MSVC] Грабли: thread_local могут конструироваться до обычных глобальных
От: Videoman Россия https://hts.tv/
Дата: 16.03.17 16:28
Оценка: 5 (1)
Здравствуйте, Alexander G, Вы писали:

AG>Где это наблюдалось?


В VS2013 наблюдается. Там в исходника рантайма явно видно — захватывается лок, а зачем начинаю зваться деструкторы глобальных объектов. _endthreadex — пытается получить этоже лок, чтобы очистить свои.

AG>В DLL так нельзя делать, да.

Я в курсе. Но там могут быть еще пляски с attach/detach потоков.

AG>В приложении — можно.

Вы уверены??? У меня ваш код стабильно виснет.
Re[4]: [MSVC] Грабли: thread_local могут конструироваться до обычных глобальных
От: Alexander G Украина  
Дата: 16.03.17 17:20
Оценка:
Здравствуйте, Videoman, Вы писали:

V>Вы уверены??? У меня ваш код стабильно виснет.


Ха. Работает в VS2015.
И в gcc на ideone:
http://ideone.com/EAFDQj

В VS2012 — виснет в рантайме на _lock(_EXIT_LOCK1);
Русский военный корабль идёт ко дну!
Re[5]: [MSVC] Грабли: thread_local могут конструироваться до
От: Videoman Россия https://hts.tv/
Дата: 16.03.17 19:12
Оценка: +1
Здравствуйте, Alexander G, Вы писали:

AG>В VS2012 — виснет в рантайме на _lock(_EXIT_LOCK1);


Интересно, это глюк ??? Или может быть это implementation defined? С другой стороны, мне не понятно, почему нельзя было сделать нормально. Зачем вызывать все пользовательские глобальные деструкторы во всех потоках в едином локе, необходимом для собственных нужд самого рантайма?! Ну, отпускали бы его, на момент вызова всех at_exit(...).
Отредактировано 16.03.2017 19:14 Videoman . Предыдущая версия .
Re[5]: [MSVC] Грабли: thread_local могут конструироваться до обычных глобальных
От: Кодт Россия  
Дата: 16.03.17 20:07
Оценка:
Здравствуйте, 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 могут конструироваться до
От: Videoman Россия https://hts.tv/
Дата: 17.03.17 08:21
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Не понял сценария зависания. Можешь по шагам расписать?


К>- предисловие-к-main собралось инициализировать статики

К>- залочило глобальный мьютекс
К>- в конструкторе X создали новый поток
К>- — в этом потоке полезли что-то инициализировать (какие-то внутренние статики — например, глобальную таблицу потоков или TLS-ов)
К>- — для этого надо было залочить мьютекс
К>- — опаньки!

К>Так, что ли?


Говорю как в VS2013:
1. В main создали глобальный объект. В нем, например, создали поток и запустили.
2. Выходим из main.
3. Рантайм берет глобальный лок и в нем начинает звать деструктуры глобальных объектов через механизм at_exit
4. Дойдя до деструктора глобального объекта с потоком, мы оповещается поток, тем или иным способом о том что нужно завершится и садимся на join ("виснем").
5. Поток получает сигнал и пытается завершиться. Доходит до _endthreadex.
6. _endthreadex, как я понимаю, пытается чистить глобалные объекты потока (thread_local), но перед этим пытается захватить тотже лок, что и at_exit.
7. Результат — дедлок!
Отредактировано 17.03.2017 8:22 Videoman . Предыдущая версия .
Re[6]: [MSVC] Грабли: thread_local могут конструироваться до обычных глобальных
От: Alexander G Украина  
Дата: 17.03.17 09:29
Оценка: 72 (1)
Здравствуйте, Кодт, Вы писали:

К>Не понял сценария зависания. Можешь по шагам расписать?


В 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;
}


По-старинке работает:

#include <Windows.h>
#include <process.h>

static struct X
{
    static unsigned int  __stdcall ThdFunc(void* param)
    {
        ::WaitForSingleObject( reinterpret_cast<HANDLE>(param), INFINITE );
        _endthreadex(0); // Не важно, можно так, можно и вернуть управление
        return 0;
    }

    X()
    {
        stop = ::CreateEvent(NULL, /* bManualReset = */ TRUE, /* bInitialState = */ FALSE, NULL);
        thd = reinterpret_cast<HANDLE>(_beginthreadex(NULL, 0, ThdFunc, reinterpret_cast<void*>(stop), 0, NULL));
    }

    ~X()
    {
        ::SetEvent(stop);
        ::WaitForSingleObject( reinterpret_cast<HANDLE>(thd), INFINITE );
        ::CloseHandle(thd);
        ::CloseHandle(stop);

        TCHAR message[] = _T("Exited\r\n");
        DWORD wr = 0;
        ::WriteConsole( ::GetStdHandle(STD_OUTPUT_HANDLE), message, _tcslen(message), &wr, NULL);
    }
    
    HANDLE stop;
    HANDLE thd;
} x;

int main()
{
    TCHAR message[] = _T("Exiting\r\n");
    DWORD wr = 0;
    ::WriteConsole( ::GetStdHandle(STD_OUTPUT_HANDLE), message, _tcslen(message), &wr, NULL);
    return 0;
}


Ещё один факт в пользу совета:
Хочешь std::thread в VS до 2015 — возьми лучше boost.
Русский военный корабль идёт ко дну!
Re[7]: Повесил и MSVC 2015
От: Alexander G Украина  
Дата: 17.03.17 15:59
Оценка: 3 (1)
Проблема в atexit, фактически.
смог повесить и MSVC 2015:

#include <thread>
#include <atomic>
#include <chrono>
#include <iostream>


std::atomic_bool stop_thread;
std::thread thd;

int main()
{
    thd = std::thread([]{
        while ( ! stop_thread.load() )
            std::this_thread::sleep_for(std::chrono::milliseconds(200));
        atexit([] { std::cout << "Exit thread\n"; });
    });

    atexit([]{
        stop_thread.store(true);
        thd.join();
    });

    std::cout << "Exiting\n";
    return 0;
}


gcc на ideone смог: http://ideone.com/W3JtJi

Это баг MSVC 2015 ? Создавать в connect ?
Русский военный корабль идёт ко дну!
Отредактировано 17.03.2017 16:02 Alexander G . Предыдущая версия . Еще …
Отредактировано 17.03.2017 15:59 Alexander G . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.