_beginthread和CreatThread的區別


轉自:http://www.jb51.net/article/41459.htm

我們知道在Windows下創建一個線程的方法有兩種,一種就是調用Windows API CreateThread()來創建線程;另外一種就是調用MSVC CRT的函數_beginthread()或_beginthreadex()來創建線程。相應的退出線程也有兩個函數Windows API的ExitThread()和CRT的_endthread()。這兩套函數都是用來創建和退出線程的,它們有什么區別呢?

很多開發者不清楚這兩者之間的關系,他們隨意選一個函數來用,發現也沒有什么大問題,於是就忙於解決更為緊迫的任務去了,而沒有對它們進行深究。等到有一天忽然發現一個程序運行時間很長的時候會有細微的內存泄露,開發者絕對不會想到是因為這兩套函數用混的結果。

根據Windows API和MSVC CRT的關系,可以看出來_beginthread()是對CreateThread()的包裝,它最終還是調用CreateThread()來創建線 程。那么在_beginthread()調用CreateThread()之前做了什么呢?我們可以看一下_beginthread()的源代碼,它位於 CRT源代碼中的thread.c。我們可以發現它在調用CreateThread()之前申請了一個叫_tiddata的結構,然后將這個結構用 _initptd()函數初始化之后傳遞給_beginthread()自己的線程入口函數_threadstart。_threadstart首先把由 _beginthread()傳過來的_tiddata結構指針保存到線程的顯式TLS數組,然后它調用用戶的線程入口真正開始線程。在用戶線程結束之 后,_threadstart()函數調用_endthread()結束線程。並且_threadstart還用__try/__except將用戶線程 入口函數包起來,用於捕獲所有未處理的信號,並且將這些信號交給CRT處理。

所以除了信號之外,很明顯CRT包裝Windows API線程接口的最主要目的就是那個_tiddata。這個線程私有的結構里面保存的是什么呢?我們可以從mtdll.h中找到它的定義,它里面保存的是 諸如線程ID、線程句柄、erron、strtok()的前一次調用位置、rand()函數的種子、異常處理等與CRT有關的而且是線程私有的信息。可見 MSVC CRT並沒有使用我們前面所說的__declspec(thread)這種方式來定義線程私有變量,從而防止庫函數在多線程下失效,而是采用在堆上申請一 個_tiddata結構,把線程私有變量放在結構內部,由顯式TLS保存_tiddata的指針。

了解了這些信息以后,我們應該會想到一個問題,那就是如果我們用CreateThread()創建一個線程然后調用CRT的strtok()函數, 按理說應該會出錯,因為strtok()所需要的_tiddata並不存在,可是我們好像從來沒碰到過這樣的問題。查看strtok()函數就會發現,當 一開始調用_getptd()去得到線程的_tiddata結構時,這個函數如果發現線程沒有申請_tiddata結構,它就會申請這個結構並且負責初始 化。於是無論我們調用哪個函數創建線程,都可以安全調用所有需要_tiddata的函數,因為一旦這個結構不存在,它就會被創建出來。

那么_tiddata在什么時候會被釋放呢?ExitThread()肯定不會,因為它根本不知道有_tiddata這樣一個結構存在,那么很明顯 是_endthread()釋放的,這也正是CRT的做法。不過我們很多時候會發現,即使使用CreateThread()和ExitThread() (不調用ExitThread()直接退出線程函數的效果相同),也不會發現任何內存泄露,這又是為什么呢?經過仔細檢查之后,我們發現原來密碼在CRT DLL的入口函數DllMain中。我們知道,當一個進程/線程開始或退出的時候,每個DLL的DllMain都會被調用一次,於是動態鏈接版的CRT就 有機會在DllMain中釋放線程的_tiddata。可是DllMain只有當CRT是動態鏈接版的時候才起作用,靜態鏈接CRT是沒有DllMain 的!這就是造成使用CreateThread()會導致內存泄露的一種情況,在這種情況下,_tiddata在線程結束時無法釋放,造成了泄露。

我們可以用下面這個小程序來測試:

代碼如下:
#include <Windows.h>
#include <process.h>
void thread(void *a)
{
    char* r = strtok( "aaa", "b" );
    ExitThread(0); // 這個函數是否調用都無所謂
}
int main(int argc, char* argv[])
{
    while(1) {
        CreateThread(  0, 0, (LPTHREAD_START_ROUTINE)thread, 0, 0, 0 );
        Sleep( 5 );
    }
return 0;
}

 


如 果用動態鏈接的CRT (/MD,/MDd)就不會有問題,但是,如果使用靜態鏈接CRT (/MT,/MTd),運行程序后在進程管理器中觀察它就會發現內存用量不停地上升,但是如果我們把thread()函數中的ExitThread()改 成_endthread()就不會有問題,因為_endthread()會將_tiddata()釋放。

這個問題可以總結為:當使用CRT 時(基本上所有的程序都使用CRT),請盡量使用_beginthread()/_beginthreadex()/_endthread() /_endthreadex()這組函數來創建線程。在MFC中,還有一組類似的函數是AfxBeginThread()和 AfxEndThread(),根據上面的原理類推,它是MFC層面的線程包裝函數,它們會維護線程與MFC相關的結構,當我們使用MFC類庫時,盡量使 用它提供的線程包裝函數以保證程序運行正確。


免責聲明!

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



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