CreateThread與_beginthreadex的本質差別,相信閱讀本文後你能輕松的使用多線程并能流暢準确的回答CreateThread與_beginthreadex到底有什麼差別,在實際的程式設計中到底應該使用CreateThread還是_beginthreadex?
使用多線程其實是非常容易的,下面這個程式的主線程會建立了一個子線程并等待其運作完畢,子線程就輸出它的線程ID号然後輸出一句經典名言——Hello World。整個程式的代碼非常簡短,隻有區區幾行。
[cpp]
view plain
copy
- //最簡單的建立多線程執行個體
- #include <stdio.h>
- #include <windows.h>
- //子線程函數
- DWORD WINAPI ThreadFun(LPVOID pM)
- {
- "子線程的線程ID号為:%d\n子線程輸出Hello World\n", GetCurrentThreadId());
- return 0;
- }
- //主函數,所謂主函數其實就是主線程執行的函數。
- int main()
- {
- " 最簡單的建立多線程執行個體\n");
- " -- by MoreWindo");
- HANDLE handle = CreateThread(NULL, 0, ThreadFun, NULL, 0, NULL);
- WaitForSingleObject(handle, INFINITE);
- return 0;
- }
運作結果如下所示:

下面來細講下代碼中的一些函數
第一個CreateThread
函數功能:建立線程
函數原型:
HANDLEWINAPICreateThread(
LPSECURITY_ATTRIBUTESlpThreadAttributes,
SIZE_TdwStackSize,
LPTHREAD_START_ROUTINElpStartAddress,
LPVOIDlpParameter,
DWORDdwCreationFlags,
LPDWORDlpThreadId
);
函數說明:
第一個參數表示線程核心對象的安全屬性,一般傳入NULL表示使用預設設定。
第二個參數表示線程棧空間大小。傳入0表示使用預設大小(1MB)。
第三個參數表示新線程所執行的線程函數位址,多個線程可以使用同一個函數位址。
第四個參數是傳給線程函數的參數。
第五個參數指定額外的标志來控制線程的建立,為0表示線程建立之後立即就可以進行排程,如果為CREATE_SUSPENDED則表示線程建立後暫停運作,這樣它就無法排程,直到調用ResumeThread()。
第六個參數将傳回線程的ID号,傳入NULL表示不需要傳回該線程ID号。
函數傳回值:
成功傳回新線程的句柄,失敗傳回NULL。
第二個WaitForSingleObject
函數功能:等待函數 – 使線程進入等待狀态,直到指定的核心對象被觸發。
函數原形:
DWORDWINAPIWaitForSingleObject(
HANDLEhHandle,
DWORDdwMilliseconds
);
函數說明:
第一個參數為要等待的核心對象。
第二個參數為最長等待的時間,以毫秒為機關,如傳入5000就表示5秒,傳入0就立即傳回,傳入INFINITE表示無限等待。
因為線程的句柄線上程運作時是未觸發的,線程結束運作,句柄處于觸發狀态。是以可以用WaitForSingleObject()來等待一個線程結束運作。
函數傳回值:
在指定的時間内對象被觸發,函數傳回WAIT_OBJECT_0。超過最長等待時間對象仍未被觸發傳回WAIT_TIMEOUT。傳入參數有錯誤将傳回WAIT_FAILED
CreateThread()函數是Windows提供的API接口,在C/C++語言另有一個建立線程的函數_beginthreadex(),在很多書上(包括《Windows核心程式設計》)提到過盡量使用_beginthreadex()來代替使用CreateThread(),這是為什麼了?下面就來探索與發現它們的差別吧。
首先要從标準C運作庫與多線程的沖突說起,标準C運作庫在1970年被實作了,由于當時沒任何一個作業系統提供對多線程的支援。是以編寫标準C運作庫的程式員根本沒考慮多線程程式使用标準C運作庫的情況。比如标準C運作庫的全局變量errno。很多運作庫中的函數在出錯時會将錯誤代号指派給這個全局變量,這樣可以友善調試。但如果有這樣的一個代碼片段:
[cpp]
view plain
copy
- if (system("notepad.exe readme.txt") == -1)
- {
- switch(errno)
- {
- //錯誤處理代碼
- }
- }
假設某個線程A在執行上面的代碼,該線程在調用system()之後且尚未調用switch()語句時另外一個線程B啟動了,這個線程B也調用了标準C運作庫的函數,不幸的是這個函數執行出錯了并将錯誤代号寫入全局變量errno中。這樣線程A一旦開始執行switch()語句時,它将通路一個被B線程改動了的errno。這種情況必須要加以避免!因為不單單是這一個變量會出問題,其它像strerror()、strtok()、tmpnam()、gmtime()、asctime()等函數也會遇到這種由多個線程通路修改導緻的資料覆寫問題。
為了解決這個問題,Windows作業系統提供了這樣的一種解決方案——每個線程都将擁有自己專用的一塊記憶體區域來供标準C運作庫中所有有需要的函數使用。而且這塊記憶體區域的建立就是由C/C++運作庫函數_beginthreadex()來負責的。下面列出_beginthreadex()函數的源代碼(我在這份代碼中增加了一些注釋)以便讀者更好的了解_beginthreadex()函數與CreateThread()函數的差別。
[cpp]
view plain
copy
- //_beginthreadex源碼整理By MoreWind
- _MCRTIMP uintptr_t __cdecl _beginthreadex(
- void *security,
- unsigned stacksize,
- void *),
- void * argument,
- unsigned createflag,
- unsigned *thrdaddr
- )
- {
- //pointer to per-thread data 見注1
- uintptr_t thdl; //thread handle 線程句柄
- long err = 0L; //Return from GetLastError()
- //dummy returned thread ID 線程ID号
- // validation section 檢查initialcode是否為NULL
- _VALIDATE_RETURN(initialcode != NULL, EINVAL, 0);
- //Initialize FlsGetValue function pointer
- __set_flsgetvalue();
- //Allocate and initialize a per-thread data structure for the to-be-created thread.
- //相當于new一個_tiddata結構,并賦給_ptiddata指針。
- if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL )
- goto error_return;
- // Initialize the per-thread data
- //初始化線程的_tiddata塊即CRT資料區域 見注2
- _initptd(ptd, _getptd()->ptlocinfo);
- //設定_tiddata結構中的其它資料,這樣這塊_tiddata塊就與線程聯系在一起了。
- void *) initialcode; //線程函數位址
- //傳入的線程參數
- uintptr_t)(-1);
- #if defined (_M_CEE) || defined (MRTDLL)
- if(!_getdomain(&(ptd->__initDomain))) //見注3
- {
- goto error_return;
- }
- #endif // defined (_M_CEE) || defined (MRTDLL)
- // Make sure non-NULL thrdaddr is passed to CreateThread
- if ( thrdaddr == NULL )//判斷是否需要傳回線程ID号
- thrdaddr = &dummyid;
- // Create the new thread using the parameters supplied by the caller.
- //_beginthreadex()最終還是會調用CreateThread()來向系統申請建立線程
- if ( (thdl = (uintptr_t)CreateThread(
- (LPSECURITY_ATTRIBUTES)security,
- stacksize,
- _threadstartex,
- LPVOID)ptd,
- createflag,
- LPDWORD)thrdaddr))
- uintptr_t)0 )
- {
- err = GetLastError();
- goto error_return;
- }
- //Good return
- return(thdl); //線程建立成功,傳回新線程的句柄.
- //Error return
- error_return:
- //Either ptd is NULL, or it points to the no-longer-necessary block
- //calloc-ed for the _tiddata struct which should now be freed up.
- //回收由_calloc_crt()申請的_tiddata塊
- _free_crt(ptd);
- // Map the error, if necessary.
- // Note: this routine returns 0 for failure, just like the Win32
- // API CreateThread, but _beginthread() returns -1 for failure.
- //校正錯誤代号(可以調用GetLastError()得到錯誤代号)
- if ( err != 0L )
- _dosmaperr(err);
- return( (uintptr_t)0 ); //傳回值為NULL的效句柄
- }
講解下部分代碼:
注1._ptiddataptd;中的_ptiddata是個結構體指針。在mtdll.h檔案被定義:
typedefstruct_tiddata * _ptiddata
微軟對它的注釋為Structure for each thread's data。這是一個非常大的結構體,有很多成員。本文由于篇幅所限就不列出來了。
注2._initptd(ptd, _getptd()->ptlocinfo);微軟對這一句代碼中的getptd()的說明為:
/* return address of per-thread CRT data */
_ptiddata __cdecl_getptd(void);
對_initptd()說明如下:
/* initialize a per-thread CRT data block */
void__cdecl_initptd(_Inout_ _ptiddata _Ptd,_In_opt_ pthreadlocinfo _Locale);
注釋中的CRT (C Runtime Library)即标準C運作庫。
注3.if(!_getdomain(&(ptd->__initDomain)))中的_getdomain()函數代碼可以在thread.c檔案中找到,其主要功能是初始化COM環境。
由上面的源代碼可知,_beginthreadex()函數在建立新線程時會配置設定并初始化一個_tiddata塊。這個_tiddata塊自然是用來存放一些需要線程獨享的資料。事實上新線程運作時會首先将_tiddata塊與自己進一步關聯起來。然後新線程調用标準C運作庫函數如strtok()時就會先取得_tiddata塊的位址再将需要保護的資料存入_tiddata塊中。這樣每個線程就隻會通路和修改自己的資料而不會去篡改其它線程的資料了。是以,如果在代碼中有使用标準C運作庫中的函數時,盡量使用_beginthreadex()來代替CreateThread()。相信閱讀到這裡時,你會對這句簡短的話有個非常深刻的印象,如果有面試官問起,你也可以流暢準确的回答了^_^。
接下來,類似于上面的程式用CreateThread()建立輸出“Hello World”的子線程,下面使用_beginthreadex()來建立多個子線程:
[cpp]
view plain
copy
- //建立多子個線程執行個體
- #include <stdio.h>
- #include <process.h>
- #include <windows.h>
- //子線程函數
- unsigned int __stdcall ThreadFun(PVOID pM)
- {
- "線程ID号為%4d的子線程說:Hello World\n", GetCurrentThreadId());
- return 0;
- }
- //主函數,所謂主函數其實就是主線程執行的函數。
- int main()
- {
- " 建立多個子線程執行個體 \n");
- " -- by MoreWindows(
- const int THREAD_NUM = 5;
- HANDLE handle[THREAD_NUM];
- for (int i = 0; i < THREAD_NUM; i++)
- HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL);
- WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
- return 0;
- }
運作結果如下:
圖中每個子線程說的都是同一句話,不太好看。能不能來一個線程報數功能,即第一個子線程輸出1,第二個子線程輸出2,第三個子線程輸出3,……。要實作這個功能似乎非常簡單——每個子線程對一個全局變量進行遞增并輸出就可以了。代碼如下:
[cpp]
view plain
copy
- //子線程報數
- #include <stdio.h>
- #include <process.h>
- #include <windows.h>
- int g_nCount;
- //子線程函數
- unsigned int __stdcall ThreadFun(PVOID pM)
- {
- g_nCount++;
- "線程ID号為%4d的子線程報數%d\n", GetCurrentThreadId(), g_nCount);
- return 0;
- }
- //主函數,所謂主函數其實就是主線程執行的函數。
- int main()
- {
- " 子線程報數 \n");
- " -- by MoreWindow
- const int THREAD_NUM = 10;
- HANDLE handle[THREAD_NUM];
- g_nCount = 0;
- for (int i = 0; i < THREAD_NUM; i++)
- HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL);
- WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
- return 0;
- }
對一次運作結果截圖如下: