线程对象的 WaitForSingleObject 在 DLL 卸载中不起作用



我偶然发现了卸载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;
}

最新更新