CreateThread和_beginthread的區別及使用

CreateThread 是一個Win 32API 函數,_beginthread 是一個CRT(C Run-Time)函數,他們都是實現多線城的創建的函數,而且他們擁有相同的使用方法,相同的參數列表。
但是他們有什麼區別呢?

一般來說,從使用角度是沒有多大的區別的,CRT函數中除了signal()函數不能在CreateThread創建的線城中使用外,其他的CRT函數都可一正常使用,但是如果在CreateThread創建的線城中使用CRT函數的話,會產生一些Memory Leak.

用CreateThread 創建的線城能否被CRT函數 _endthreadex() 關閉?

CreateThread()和_beginthreadex()在Jeffrey的《Windows核心編程》中講的很清楚,應當儘量避免使用CreateThread()

事實上,_beginthreadex()在內部先爲線程創建一個線程特有的tiddata結構,然後調用CreateThread()。在某些非線程安全的CRT函數中會請求這個結構。如果直接使用CreateThread()的話,那些函數發現請求的tiddata爲NULL,就會在現場爲該線程創建該結構,此後調用EndThread()時會引起內存泄漏。_endthreadex()可以釋放由CreateThread()創建的線程,實際上,在它的內部會先釋放由_beginthreadex()創建的tiddata結構,然後調用EndThread()。

因此,應當使用_beginthreadex()和_endthreadex(),而避免使用CreateThread()和EndThread()。當然,_beginthread()和_endthread()也是應當避免使用的。

程序員對於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):

#include 
#include 
#include 
#include
volatile bool threadStarted = false;
void leaker()
{
    std::cout << atoi( "0" ) << std::endl;
}
DWORD __stdcall CreateThreadFunc( LPVOID )
{
    leaker();
    threadStarted = false;
    return 0;
}
DWORD __stdcall CreateThreadFuncWithEndThread( LPVOID )
{
    leaker();
    threadStarted = false;
    _endthread();
    return 0;
}
void __cdecl beginThreadFunc( LPVOID )
{
    leaker();
    threadStarted = false;
}
int main()
{
    for(;;)
    {
        while( threadStarted )
            Sleep( 5 );
        threadStarted = true;
//      _beginthread( beginThreadFunc, 0, 0 );//1
        CreateThread( NULL, 0, CreateThreadFunc, 0, 0, 0 );//2
//      CreateThread( NULL, 0, CreateThreadFuncWithEndThread, 0, 0, 0 );//3
    }
    return 0;
}

如果你用VC的多線程+靜態鏈接CRT選項去編譯這個程序,並且嘗試打開1、2、3之中的一行,你會發覺只有2打開的情況下,程序纔會發生內存泄漏(可以在Task Manager裏面明顯的觀察到)。3之所以不會出現內存泄漏是因爲主動調用了_endthread。

VII. 總結

如果你使用了DLL方式鏈接的CRT庫,或者你只是一次性創建少量的線程,那麼你或許可以採取鴕鳥策略,忽視這個問題。上面一節代碼中第3種方法基於對CRT庫的瞭解,但是並不保證這是一個好的方法,因爲每一個版本的VC的CRT可能都會有些改變。看來,除非你的頭腦清晰到可以記住這一切,或者你可以不厭其煩的每調用一個C函數都查一下CRT代碼,否則總是使用_beginthread(或者它的兄弟_beginthreadex)是一個不錯的選擇。

[後記]
網友condor指出本文的一個錯誤:在dllcrt0.c中,DllMain的Thread Detach所釋放的ptd,其實是dllcrt0.c的DllMain中的Thread Attach所創建的。也就是說,當你用CRT DLL的時候,DllMain對線程做了一切初始化/清除工作。我查看源代碼,thread.c中的_threadstart函數,在設置TLS之前做了檢查,這其實就是爲了避免重複設置導致的內存泄漏。

發佈了98 篇原創文章 · 獲贊 68 · 訪問量 29萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章