我偶然发现了卸载DLL时Windows线程机制的意外行为。有一包工作线程对象,我正在尝试在卸载 DLL 时优雅地完成它们(通过 DllMain DLL_PROCESS_DETACH)。代码非常简单(我确实发送了一个事件来完成线程的等待循环):
WaitForSingleObject( ThrHandle, INFINITE );
CloseHandle( ThrHandle );
然而,WaitForSingleObject挂起了整个事情。如果我在卸载 DLL 之前执行它,它工作正常。如何修复此行为?
你不能等待线程在 DllMain() 中退出。 除非在收到DLL_PROCESS_DETACH时线程已经退出,否则这样做将始终死锁。 这是预期的行为。
原因是对 DllMain() 的调用是通过加载程序锁序列化的。 当调用 ExitThread() 时,它会声明加载程序锁,以便它可以调用 DllMain() DLL_THREAD_DETACH。 在该调用完成之前,线程仍在运行。
所以 DllMain 正在等待线程退出,线程正在等待 DllMain 退出,这是一种经典的死锁情况。
另请参阅 MSDN 上的动态链接库最佳做法。
解决方案是在卸载 DLL 之前向 DLL 添加一个新函数,供应用程序调用。 正如您所指出的,您的代码在显式调用时已经运行良好。
如果向后兼容性要求导致无法添加此类函数,并且必须具有工作线程,请考虑将 DLL 拆分为两部分,其中一部分由另一部分动态加载。 动态加载的部分将(至少)包含工作线程所需的所有代码。
当应用程序本身加载的 DLL 收到DLL_PROCESS_DETACH时,只需将事件设置为向线程发出退出信号,然后立即返回。 必须指定其中一个线程等待所有其他线程,然后释放第二个 DLL,您可以使用 FreeLibraryAndExitThread() 安全地执行此操作。
(根据具体情况,特别是如果工作线程正在退出和/或作为常规操作的一部分创建新线程,您可能需要非常小心以避免争用条件和/或死锁;如果您使用线程池和回调而不是手动创建工作线程,这可能会更简单。
在线程不需要使用任何最简单的 Windows API 的特殊情况下,可以使用线程池和工作回调来避免需要第二个 DLL。 一旦回调退出,你可以使用 WaitForThreadpoolWorkCallbacks() 进行检查,卸载库是安全的 - 你不需要等待线程本身退出。
这里的问题是,回调必须避免任何可能采用加载程序锁的 Windows API。 没有记录哪些 API 调用在这方面是安全的,并且它因不同版本的 Windows 而异。 例如,如果要调用比 SetEvent 或 WriteFile 更复杂的任何内容,或者如果您使用的是库而不是本机 Windows API 函数,则不得使用此方法。
当我尝试将代码注入另一个桌面进程时,我遇到了这样的问题,WaitForSingleObject 会导致我的线程内死锁。我通过捕获窗口的默认消息程序解决了这个问题,希望对其他人有所帮助。
#define WM_INSIDER (WM_USER + 2021)
WNDPROC prev_proc = nullptr;
HWND FindTopWindow(DWORD pid)
{
struct Find { HWND win; DWORD pid; } find = { nullptr, pid };
EnumWindows([](HWND hwnd, LPARAM lParam) -> BOOL {
auto p = (Find*)(lParam);
DWORD id;
if (GetWindowThreadProcessId(hwnd, &id) && id == p->pid) {
// done
p->win = hwnd;
return FALSE;
}
// continue
return TRUE;
}, (LPARAM)&find);
return find.win;
}
// thread entry
int insider(void *)
{
// do whatever you want as a normal thread
return (0);
}
LRESULT CALLBACK insider_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HANDLE t;
switch (uMsg) {
case WM_INSIDER:
t = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)insider, 0, 0, NULL);
CloseHandle(t);
break;
}
return CallWindowProc(prev_proc, hwnd, uMsg, wParam, lParam);
}
void setup() {
auto pid = GetCurrentProcessId();
auto win = FindTopWindow(pid);
prev_proc = (WNDPROC)SetWindowLongPtr(win, GWL_WNDPROC, (LONG_PTR)&insider_proc);
// signal to create thread later
PostMessage(win, WM_INSIDER, 0, 0);
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
setup();
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}