估計很多人都知道裝載DLL過程中的多線程死鎖是因為DllMain的順序調用規則,但是很少人了解解除安裝DLL過程中的多線程死鎖也是由于同樣的原因。例如,如果一個DLL的DllMain的代碼寫成下面的形式,且程序中有至少一個DLL的DllMain沒有調用DisableThreadLibraryCalls函數的話,那麼解除安裝該DLL過程中就會因為DllMain的順序操作特性帶來DLL内部線程沒有完全退出的錯誤。
上述代碼的流程是這樣的:
(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除外),程式能夠正常退出。
很顯然的一個教訓就是在DllMain内部應該避免任何Wait*調用。但是程序互斥對象的問題不僅僅限于Wait*函數。作業系統在CreateProcess、GetModuleFileName、GetProcAddress、wglMakeCurrent、LoadLibrary和FreeLibrary等函數中在背景擷取程序互斥對象,是以在DllMain中不應該調用任何這些函數。因為DllMain擷取程序互斥對象,是以一次隻能有一個線程執行DllMain。
ATL singleton的 FinalConstruct函數和FinalRelease函數分别是DllMain在響應DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH時被調用的,是以也要同樣注意本文所述的問題