Найти в Дзене
Под капотом ПО

DLL в многопоточных средах: Deadlock в DllMain и как избежать кошмара

Привет, коллеги! 👋 Вы профессиональные C++ разработчики, пишете идеальный код, но внезапно ваше приложение зависает при загрузке DLL. Виновник - deadlock в DllMain. Почему это происходит и как это исправить? Давайте разберёмся! При загрузке или выгрузке DLL система входит в критическую секцию загрузчика (Loader Lock). Ваша функция DllMain выполняется под этой блокировкой. Если вы попытаетесь сделать что-то, что требует другой блокировки (например, создание потока), - deadlock неизбежен. 💥 Типичные причины deadlock: #include <Windows.h> #include <thread> BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) { switch (reason) { case DLL_PROCESS_ATTACH: // ⚠️ Опасность! Создание потока под Loader Lock. std::thread killer_thread([]() { // Поток пытается загрузить другую DLL -> deadlock! LoadLibraryA("Another.dll"); }); killer_thread.join(); // Вечное ожидание... break; } return TRUE; } Что
Оглавление

Привет, коллеги! 👋 Вы профессиональные C++ разработчики, пишете идеальный код, но внезапно ваше приложение зависает при загрузке DLL. Виновник - deadlock в DllMain. Почему это происходит и как это исправить? Давайте разберёмся!

🔒 Почему DllMain - зона повышенной опасности?

При загрузке или выгрузке DLL система входит в критическую секцию загрузчика (Loader Lock). Ваша функция DllMain выполняется под этой блокировкой. Если вы попытаетесь сделать что-то, что требует другой блокировки (например, создание потока), - deadlock неизбежен.

💥 Типичные причины deadlock:

  1. Создание/остановка потоков в DllMain.
  2. Вызов функций, которые используют синхронизацию (мьютексы, критические секции).
  3. Обращение к другим DLL, которые могут зависеть от загрузчика.

💀 Пример-убийца: Deadlock в действии

#include <Windows.h>
#include <thread>
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
switch (reason) {
case DLL_PROCESS_ATTACH:
// ⚠️ Опасность! Создание потока под Loader Lock.
std::thread killer_thread([]() {
// Поток пытается загрузить другую DLL -> deadlock!
LoadLibraryA("Another.dll");
});
killer_thread.join(); // Вечное ожидание...
break;
}
return TRUE;
}

Что происходит:

  • Главный поток держит Loader Lock.
  • Новый поток пытается вызвать LoadLibrary, которая требует той же блокировки.
  • Результат: оба потока ждут друг друга → deadlock.

🛡️ 5 правил выживания в DllMain

  1. Минимум кода:
  2. В DllMain - только инициализация без зависимостей. Никаких сложных операций!
  3. Запрещено создавать/останавливать потоки:
  4. Используйте отложенную инициализацию. Например:
static bool is_initialized = false;
if (reason == DLL_PROCESS_ATTACH && !is_initialized) {
// Запланировать инициализацию, но не выполнять здесь!
std::call_once(init_flag, []() { /* безопасная инициализация */ });
}
  1. Не вызывайте другие DLL:
  2. Особенно если те могут зависеть от загрузчика. Используйте явную загрузку (LoadLibrary/GetProcAddress) после инициализации.
  3. Избегайте синхронизации:
  4. Мьютексы, критические секции, std::sync - под запретом. Если нужно - используйте атомарные операции.
  5. Разделяйте инициализацию:
  6. Вынесите код в отдельную функцию, которую вызовет приложение после загрузки:
// DllMain
BOOL APIENTRY DllMain(HMODULE, DWORD, LPVOID) { return TRUE; }
// Явная инициализация
extern "C" __declspec(dllexport) void SafeInit() {
// Здесь можно всё: потоки, синхронизация и т.д.
}

🔧 Шаблон безопасной DLL

#include <Windows.h>
// Флаг для отложенной инициализации
static std::once_flag g_init_flag;
BOOL APIENTRY DllMain(HMODULE, DWORD reason, LPVOID) {
if (reason == DLL_PROCESS_ATTACH) {
// Максимум что можно - запланировать инициализацию
std::call_once(g_init_flag, []() { /* безопасный код */ });
}
return TRUE;
}
// Явный вызов инициализации
extern "C" __declspec(dllexport) void InitDLL() {
std::call_once(g_init_flag, []() { /* полноценная инициализация */ });
}

🎯 Итог: Ваши действия

  • Никогда не делайте в DllMain того, что требует времени.
  • Документируйте для пользователей DLL: «Вызовите InitDLL() после загрузки!».
  • Тестируйте под нагрузкой: запускайте параллельную загрузку DLL в 10+ потоков.
💡 Помните: DllMain - не место для творчества. Чем проще, тем надежнее!

Удачи в борьбе с deadlock! 💪

Ваш код будет безупречным, а приложения - стабильными.