估計很多人都知道裝載DLL過程中的多線程死鎖是因為DllMain的順序調用規則,但是很少人了解卸載DLL過程中的多線程死鎖也是由於同樣的原因。例如,如果一個DLL的DllMain的代碼寫成下面的形式,且進程中有至少一個DLL的DllMain沒有調用DisableThreadLibraryCalls函數的話,那么卸載該DLL過程中就會因為DllMain的順序操作特性帶來DLL內部線程沒有完全退出的錯誤。
//----------------------start ------------ HANDLE g_thread_handle =NULL; // 該DLL內部線程的句柄 DWORD g_thread_id =0; // 該DLL內部線程的ID HANDLE g_hEvent=NULL;// 應答事件的句柄 DWORD WINAPI InSideDll_ThreadProc( LPVOID p ) { while(1){ // 收到通知就退出線程函數 DWORD ret = ::WaitForSingleObject( g_hEvent, INFINITE ); if(WAIT_TIMEOUT = =ret|| WAIT_OBJECT_0 = =ret) break; } return true ; } BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: //線程正在映射到進程地址空間中 { // 創建DLL內的線程使用的事件對象 g_hEvent = ::CreateEvent( NULL, FALSE, FALSE, _T("hello11" )); //創建DLL內的線程對象 g_thread_handle = ::CreateThread(NULL,0, InSideDll_ThreadProc,(LPVOID)0,0, &( g_thread_id) ) ; // 禁止線程庫調用, DisableThreadLibraryCalls((HINSTANCE)hModule); } break; case DLL_PROCESS_DETACH: // DLL正在從進程地址空間中卸載 { // 通知內部的線程g_thread_handle 退出 ::SetEvent(g_hEvent); // 等待內部的線程g_thread_handle 退出 ::WaitForSingleObject(g_thread_handle, INFINITE ) ; // 清除資源 ::CloseHandle(g_thread_handle); g_thread_id = 0 ; g_thread_handle = NULL ; ::CloseHandle(g_hEvent); g_hEvent=NULL; } break; } return TRUE; } //----------------------end ------------
上述代碼的流程是這樣的:
(1)裝載DLL時,創建一個 DLL內部的線程g_thread_handle及事件對象g_hEvent,且線程g_thread_handle在事件對象g_hEvent上等待。
(2)卸載DLL時,通過SetEvent(g_hEvent)通知線程g_thread_handle退出,隨即調用WaitForSingleObject函數等待線程g_thread_handle終止運行。如果線程g_thread_handle終止運行,則執行清除工作。
但是如果對這樣的程序進行調試,就會發現程序在退出時該DllMain沒有退出,等待了很長時間也沒有退出。
查看了一下線程Call Stack窗口,注意到程序正在等待DllMain內部的線程g_thread_handle的退出。盡管線程g_thread_handle的線程函數已經返回了,但是整個g_thread_handle線程走到了操作系統的ntdll.dll中並沒有完全終止,從而導致整個DLL不能順利釋放。
線程g_thread_handle為什么沒有完全退出呢?
原來,線程函數返回時,系統並不立即將它撤消。相反,系統要取出這個即將被撤消的線程,讓它調用已經映射的DLL的所有帶有DLL_THREAD_DETACH值的、且沒有調用DisableThreadLibraryCalls函數的DllMain函數。DLL_THREAD_DETACH通知告訴所有的DLL執行每個線程的清除操作,例如,DLL版本的C/C++運行期庫能夠釋放它用於管理多線程應用程序的數據塊。DisableThreadLibraryCalls函數告訴系統說,特定的DLL的DllMain函數不用接收DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知。
但是,系統是順序調用DLL的DllMain函數的。
當線程函數返回時,系統檢查進程中是否存在沒有調用DisableThreadLibraryCalls函數的DllMain函數,如果存在,系統就以進程的互斥對象的句柄作為第一個參數,在線程內部調用WaitForSingleObject函數。一旦這個將要終止運行的線程擁有該進程互斥對象,系統就讓該線程用DLL_THREAD_DETACH的值依次調用每個沒有調用DisableThreadLibraryCalls函數的DLL的DllMain函數。此后,系統才釋放對進程互斥對象的所有權。
在本例所述的應用程序中,進程的退出引起操作系統獲取進程互斥對象使操作系統可以為DLL_PROCESS_DETACH通知調用DllMain()。該DLL的DllMain()函數通知線程g_thread_handle終止運行。無論何時當進程終止一個線程時,操作系統將獲取進程互斥對象,以便於它可以為DLL_THREAD_DETACH通知調用每個加載的、沒有調用DisableThreadLibraryCalls函數的DLL的DllMain函數。在這個特定的程序中,線程g_thread_handle當線程函數返回后就阻塞了,因為CMySingleton的DllMain()所處的線程還保持着進程互斥對象。不幸的是,DllMain所處的線程然后調用WaitForSingleObject確認g_thread_handle線程是否完全終止。因為g_thread_handle線程被阻塞在進程互斥對象上,這個進程互斥對象還被DllMain線程所持有, DllMain線程要等待g_thread_handle線程從而也被阻塞,結果就導致了死鎖。如下圖所示:
注意,從圖2可以看出,如果當前進程中的所有 DLL都調用了DisableThreadLibraryCalls函數,那么上述代碼中的DLL也能正常退出。曾經寫過一個程序,除了加載一個這樣有問題的DLL沒有加載其他DLL(系統的DLL除外),程序能夠正常退出。
3、結論
很顯然的一個教訓就是在DllMain內部應該避免任何Wait*調用。但是進程互斥對象的問題不僅僅限於Wait*函數。操作系統在CreateProcess、GetModuleFileName、GetProcAddress、wglMakeCurrent、LoadLibrary和FreeLibrary等函數中在后台獲取進程互斥對象,因此在DllMain中不應該調用任何這些函數。因為DllMain獲取進程互斥對象,所以一次只能有一個線程執行DllMain。
ATL singleton的 FinalConstruct函數和FinalRelease函數分別是DllMain在響應DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH時被調用的,所以也要同樣注意本文所述的問題