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

HX_ZXHY發表於2020-09-23

很多開發者不清楚這兩者之間的關係,他們隨意選一個函式來用,發現也沒有什麼大問題,於是就忙於解決更為緊迫的任務去了。等到有一天忽然發現一個程式執行時間很長的時候會有細微的記憶體洩露,開發者絕對不會想到是因為這兩套函式用混的結果

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

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

相關文章