秒殺多執行緒第二篇 多執行緒第一次親密接觸 CreateThread與 beginthreadex本質區別
本文將帶領你與多執行緒作第一次親密接觸,並深入分析CreateThread與_beginthreadex的本質區別,相信閱讀本文後你能輕鬆的使用多執行緒並能流暢準確的回答CreateThread與_beginthreadex到底有什麼區別,在實際的程式設計中到底應該使用CreateThread還是_beginthreadex?
使用多執行緒其實是非常容易的,下面這個程式的主執行緒會建立了一個子執行緒並等待其執行完畢,子執行緒就輸出它的執行緒ID號然後輸出一句經典名言——Hello World。整個程式的程式碼非常簡短,只有區區幾行。
//最簡單的建立多執行緒例項#include <stdio.h> #include <windows.h>//子執行緒函式DWORD WINAPI ThreadFun(LPVOID pM){ printf("子執行緒的執行緒ID號為:%d\n子執行緒輸出Hello World\n", GetCurrentThreadId()); return 0;}//主函式,所謂主函式其實就是主執行緒執行的函式。int main(){ printf(" 最簡單的建立多執行緒例項\n"); printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); 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,
LPDWORD
);
函式說明:
第一個引數表示執行緒核心物件的安全屬性,一般傳入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。很多執行庫中的函式在出錯時會將錯誤代號賦值給這個全域性變數,這樣可以方便除錯。但如果有這樣的一個程式碼片段:
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()函式的區別。
//_beginthreadex原始碼整理By MoreWindows( http://blog.csdn.net/MoreWindows )_MCRTIMP uintptr_t __cdecl _beginthreadex( void *security, unsigned stacksize, unsigned (__CLR_OR_STD_CALL * initialcode) (void *), void * argument, unsigned createflag, unsigned *thrdaddr){ _ptiddata ptd; //pointer to per-thread data 見注1 uintptr_t thdl; //thread handle 執行緒控制代碼 unsigned long err = 0L; //Return from GetLastError() unsigned dummyid; //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塊就與執行緒聯絡在一起了。 ptd->_initaddr = (void *) initialcode; //執行緒函式地址 ptd->_initarg = argument; //傳入的執行緒引數 ptd->_thandle = (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 returnerror_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()來建立多個子執行緒:
//建立多子個執行緒例項#include <stdio.h>#include <process.h>#include <windows.h>//子執行緒函式unsigned int __stdcall ThreadFun(PVOID pM){ printf("執行緒ID號為%4d的子執行緒說:Hello World\n", GetCurrentThreadId()); return 0;}//主函式,所謂主函式其實就是主執行緒執行的函式。int main(){ printf(" 建立多個子執行緒例項 \n"); printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); const int THREAD_NUM = 5; HANDLE handle[THREAD_NUM]; for (int i = 0; i < THREAD_NUM; i++) handle[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL); WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); return 0;}
執行結果如下:
圖中每個子執行緒說的都是同一句話,不太好看。能不能來一個執行緒報數功能,即第一個子執行緒輸出1,第二個子執行緒輸出2,第三個子執行緒輸出3,……。要實現這個功能似乎非常簡單——每個子執行緒對一個全域性變數進行遞增並輸出就可以了。程式碼如下:
//子執行緒報數#include <stdio.h>#include <process.h>#include <windows.h>int g_nCount;//子執行緒函式unsigned int __stdcall ThreadFun(PVOID pM){ g_nCount++; printf("執行緒ID號為%4d的子執行緒報數%d\n", GetCurrentThreadId(), g_nCount); return 0;}//主函式,所謂主函式其實就是主執行緒執行的函式。int main(){ printf(" 子執行緒報數 \n"); printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); const int THREAD_NUM = 10; HANDLE handle[THREAD_NUM]; g_nCount = 0; for (int i = 0; i < THREAD_NUM; i++) handle[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL); WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); return 0;}
對一次執行結果截圖如下:
顯示結果從1數到10,看起來好象沒有問題。
答案是不對的,雖然這種做法在邏輯上是正確的,但在多執行緒環境下這樣做是會產生嚴重的問題,下一篇《秒殺多執行緒第三篇 原子操作 Interlocked系列函式》將為你演示錯誤的結果(可能非常出人意料)並解釋產生這個結果的詳細原因。
如果覺得本文對您有幫助,請點選‘頂’支援一下,您的支援是我寫作最大的動力,謝謝。