/*------------------------------------------------------------------------------------------------- 摘錄時間 2017-03-03; _beginthread 和 CreateThread 區別; 程序員對於Windows程序中應該用_beginthread還是CreateThread來創建線程,一直有所爭論; 本文將從對CRT源代碼出發探討這個問題; I. 起因; 今天一個朋友問我程序中究竟應該使用_beginthread還是CreateThread,並且告訴我如果使用不當可能會有內存泄漏; 其實我過去對這個問題也是一知半解,為了對朋友負責,專門翻閱了一下VC的運行庫(CRT)源代碼,終於找到了答案; II. CRT; CRT(C/C++ Runtime Library)是支持C/C++運行的一系列函數和代碼的總稱; 雖然沒有一個很精確的定義,但是可以知道,你的main就是它負責調用的,你平時調用的諸如strlen、strtok、time、atoi之類的函數也是它提供的; 我們以Microsoft Visual.NET 2003中所附帶的CRT為例; 假設你的.NET 2003安裝在C:Program FilesMicrosoft Visual Studio.NET 2003中,那么CRT的源代碼就在C : Program FilesMicrosoft Visual Studio.NET 2003Vc7crtsrc中; 既然有了這些實現的源代碼,我們就可以找到一切解釋了; III. _beginthread / _endthread; 這個函數究竟做了什么呢?它的代碼在thread.c中; 閱讀代碼,可以看到它最終也是通過CreateThread來創建線程的,主要區別在於,它先分配了一個_tiddata,並且調用了_initptd來初始化這個分配了的指針; 而這個指針最后會被傳遞到CRT的線程包裝函數_threadstart中,在那里會把這個指針作為一個TLS(Thread Local Storage)保存起來,然后_threadstart會調用我們傳入的線程函數,並且在那個函數退出后調用_endthread; 這里也可以看到,_threadstart用一個__try / __except塊把我們的函數包了起來,並且在發生異常的時候,調用exit退出。(_threadstart和endthread的代碼都在thread.c中) 這個_tiddata是一個什么樣的結構呢? 它在mtdll.h中定義,它的成員被很多CRT函數所用到,譬如int _terrno,這是這個線程中的錯誤標志;char* _token,strtok以來這個變量記錄跨函數調用的信息... 那么_endthread又做了些什么呢? 除了調用浮點的清除代碼以外,它還調用了_freeptd來釋放和這個線程相關的tiddata。也就是說,在 _beginthread里面分配的這塊內存,以及在線程運行過程中其它CRT函數中分配並且記錄在這個內存結構中的內存,在這里被釋放了。 通過上面的代碼,我們可以看到,如果我使用_beginthread函數創建了線程,它會為我創建好CRT函數需要的一切,並且最后無需我操心,就可以把清除工作做得很好; 可能唯一需要注意的就是,如果需要提前終止線程,最好是調用_endthread或者是返回,而不要調用ExitThread,因為這可能造成內存釋放不完全; 同時我們也可以看出,如果我們用CreateThread函數創建了線程,並且不對C運行庫進行調用(包括任何間接調用),就不必擔心什么問題了; IV. CreateThread和CRT 或許有人會說,我用CreateThread創建線程以后,我也調用了C運行庫函數,並且也使用ExitThread退出了,可是我的程序運行得好好的,既沒有因為CRT沒有初始化而崩潰,也沒有因為忘記調用 _endthread而發生內存泄漏. 這是為什么呢,讓我們繼續我們的CRT之旅。 假設我用CreateThread創建了一個線程,我調用 strtok函數來進行字符串處理,這個函數肯定是需要某些額外的運行時支持的。strtok的源代碼在strtok.c中。 從代碼可見,在多線程情況下,strtok的第一句有效代碼就是_ptiddata ptd = _getptd(),它通過這個來獲得當前的ptd. 可是我們並沒有通過_beginthread來創建ptd,那么一定是_getptd搗鬼了。打開 tidtable.c,可以看到_getptd的實現,果然,它先嘗試獲得當前的ptd,如果不能,就重新創建一個。因此,后續的CRT調用就安全了。 可是這塊ptd最終又是誰釋放的呢?打開dllcrt0.c,可以看到一個DllMain函數。在VC中,CRT既可以作為一個動態鏈接庫和主程序鏈接,也可以作為一個靜態庫和主程序鏈接,這個在Project Setting->Code Generations里面可以選。 當CRT作為DLL鏈接到主程序時,DllMain就是CRT DLL的入口。Windows的DllMain可以由四種原因調用:Process Attach / Process Detach / Thread Attach / Thread Detach; 最后一個,也就是當線程函數退出后但是線程還沒有銷毀前,會在這個線程的上下文中用Thread Detach調用DllMain,這里,CRT做了一個_freeptd(NULL),也就是說,如果有ptd,就free掉; 所以說,恰巧沒有發生內存泄漏是因為你用的是動態鏈接的CRT。 於是我們得出了一個更精確的結論,如果我沒有使用那些會使用_getptd的CRT函數,使用CreateThread就是安全的。 V. 使用ptd的函數; 那么,究竟那些函數使用了_getptd呢?很多!在CRT目錄下搜索_getptd,你會發覺很多意想不到的函數都用到了它,除了strtok、rand這類需要保持狀態的,還有所有的字符串相關函數,因為它們要用到ptd中的locale信息; 所有的mbcs函數,因為它們要用到ptd中的mbcs信息...; VI. 測試代碼; 最下面是一段測試代碼(leaker中用到了atoi,它需要ptd); 如果你用VC的多線程+靜態鏈接CRT選項去編譯這個程序,並且嘗試打開1、2、3之中的一行,你會發覺只有2打開的情況下,程序才會發生內存泄漏(可以在Task Manager里面明顯的觀察到); 3之所以不會出現內存泄漏是因為主動調用了_endthread; VII. 總結; 如果你使用了DLL方式鏈接的CRT庫,或者你只是一次性創建少量的線程,那么你或許可以采取鴕鳥策略,忽視這個問題; 上面一節代碼中第3種方法基於對CRT庫的了解,但是並不保證這是一個好的方法,因為每一個版本的VC的CRT可能都會有些改變; 看來,除非你的頭腦清晰到可以記住這一切,或者你可以不厭其煩的每調用一個C函數都查一下CRT代碼,否則總是使用 _beginthread(或者它的兄弟_beginthreadex)是一個不錯的選擇; VIII. 后記; 網友condor指出本文的一個錯誤:在dllcrt0.c 中,DllMain的Thread Detach所釋放的ptd,其實是dllcrt0.c的DllMain中的Thread Attach所創建的; 也就是說,當你用CRT DLL的時候,DllMain對線程做了一切初始化 / 清除工作。我查看源代碼,thread.c中的_threadstart函數,在設置TLS之前做了檢查,這其實就是為了避免重復設置導致的內存泄漏; -------------------------------------------------------------------------------------------------*/ // MyThread: my_thread.h /*------------------------------------------------------------------------------------------------- #include <process.h> #include <iostream> #include <CRTDBG.H> #include <wtypes.h> #ifdef MYTHREAD_EXPORTS #define MYTHREAD_API _declspec(dllexport) #else #define MYTHREAD_API _declspec(dllimport) #endif extern volatile bool threadStarted = false; MYTHREAD_API void my_printf(); MYTHREAD_API DWORD __stdcall CreateThreadFunc(LPVOID); MYTHREAD_API DWORD __stdcall CreateThreadFuncWithEndThread(LPVOID); MYTHREAD_API void __cdecl beginThreadFunc(LPVOID); -------------------------------------------------------------------------------------------------*/ // MyThread: my_thread.cpp /*------------------------------------------------------------------------------------------------- #include "my_thread.h" void my_printf() { std::cout << atoi("0") << std::endl; } DWORD __stdcall CreateThreadFunc(LPVOID) { my_printf(); threadStarted = false; return 0; } DWORD __stdcall CreateThreadFuncWithEndThread(LPVOID) { my_printf(); threadStarted = false; _endthread(); return 0; } void __cdecl beginThreadFunc(LPVOID) { my_printf(); threadStarted = false; } -------------------------------------------------------------------------------------------------*/ // Thread_Test: thread_test.cpp /*------------------------------------------------------------------------------------------------- #include "my_thread.h" #define BEGINTHREAD_TEST #define CREATETHREAD_TEST #define CREATETHREAD_WITHENDTHREAD_TEST int main() { while (1) { while (threadStarted) { Sleep(5); } threadStarted = true; #ifdef BEGINTHREAD_TEST _beginthread( beginThreadFunc, 0, 0 );//1 #endif #ifdef CREATETHREAD_TEST CreateThread(NULL, 0, CreateThreadFunc, 0, 0, 0);//2 #endif #ifdef CREATETHREAD_WITHENDTHREAD_TEST CreateThread( NULL, 0, CreateThreadFuncWithEndThread, 0, 0, 0 );//3 #endif break; } getchar(); return 0; } -------------------------------------------------------------------------------------------------*/