CreateThread()與_beginthread()的區別詳細解析

轉自:https://blog.csdn.net/qq_22642239/article/details/90445414

很多開發者不清楚這兩者之間的關係,他們隨意選一個函數來用,發現也沒有什麼大問題,

於是就忙於解決更爲緊迫的任務去了。等到有一天忽然發現一個程序運行時間很長的時候會有細微的內存泄露,

開發者絕對不會想到是因爲這兩套函數用混的結果

 

我們知道在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類庫時,儘量使用它提供的線程包裝函數以保證程序運行正確。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章