MFC筆記(四)——多執行緒程式設計3:用_beginthreadex()來代替使用CreateThread()
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 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._ptiddata ptd;中的_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系列函式》將為你演示錯誤的結果(可能非常出人意料)並解釋產生這個結果的詳細原因。
轉自:https://blog.csdn.net/morewindows/article/details/7421759