<轉>DllMain和多線程死鎖


估計很多人都知道裝載DLL過程中的多線程死鎖是因為DllMain的順序調用規則,但是很少人了解卸載DLL過程中的多線程死鎖也是由於同樣的原因。例如,如果一個DLLDllMain的代碼寫成下面的形式,且進程中有至少一個DLLDllMain沒有調用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函數告訴系統說,特定的DLLDllMain函數不用接收DLL_THREAD_ATTACHDLL_THREAD_DETACH通知。

但是,系統是順序調用DLLDllMain函數的。

當線程函數返回時,系統檢查進程中是否存在沒有調用DisableThreadLibraryCalls函數的DllMain函數,如果存在,系統就以進程的互斥對象的句柄作為第一個參數,在線程內部調用WaitForSingleObject函數。一旦這個將要終止運行的線程擁有該進程互斥對象,系統就讓該線程用DLL_THREAD_DETACH的值依次調用每個沒有調用DisableThreadLibraryCalls函數的DLLDllMain函數。此后,系統才釋放對進程互斥對象的所有權。

在本例所述的應用程序中,進程的退出引起操作系統獲取進程互斥對象使操作系統可以為DLL_PROCESS_DETACH通知調用DllMain()。該DLLDllMain()函數通知線程g_thread_handle終止運行。無論何時當進程終止一個線程時,操作系統將獲取進程互斥對象,以便於它可以為DLL_THREAD_DETACH通知調用每個加載的、沒有調用DisableThreadLibraryCalls函數的DLLDllMain函數。在這個特定的程序中,線程g_thread_handle當線程函數返回后就阻塞了,因為CMySingletonDllMain()所處的線程還保持着進程互斥對象。不幸的是,DllMain所處的線程然后調用WaitForSingleObject確認g_thread_handle線程是否完全終止。因為g_thread_handle線程被阻塞在進程互斥對象上,這個進程互斥對象還被DllMain線程所持有, DllMain線程要等待g_thread_handle線程從而也被阻塞,結果就導致了死鎖。如下圖所示:

 

 

注意,從圖2可以看出,如果當前進程中的所有 DLL都調用了DisableThreadLibraryCalls函數,那么上述代碼中的DLL也能正常退出。曾經寫過一個程序,除了加載一個這樣有問題的DLL沒有加載其他DLL(系統的DLL除外),程序能夠正常退出。

3、結論

    很顯然的一個教訓就是在DllMain內部應該避免任何Wait*調用。但是進程互斥對象的問題不僅僅限於Wait*函數。操作系統在CreateProcessGetModuleFileNameGetProcAddresswglMakeCurrentLoadLibraryFreeLibrary等函數中在后台獲取進程互斥對象,因此在DllMain中不應該調用任何這些函數。因為DllMain獲取進程互斥對象,所以一次只能有一個線程執行DllMain

       ATL singleton FinalConstruct函數和FinalRelease函數分別是DllMain在響應DLL_PROCESS_ATTACHDLL_PROCESS_DETACH時被調用的,所以也要同樣注意本文所述的問題

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM