本文將帶領你與多線程做第一次親密接觸,並深刻分析CreateThread與_beginthreadex的本質區別,相信閱讀本文後你能輕鬆的使用多線程並能流暢準確的回答CreateThread與_beginthreadex到底有什麼區別,在實際的編程中到底應該使用CreateThread仍是_beginthreadex? 程序員
使用多線程實際上是很是容易的,下面這個程序的主線程會建立了一個子線程並等待其運行完畢,子線程就輸出它的線程ID號而後輸出一句經典名言——Hello World。整個程序的代碼很是簡短,只有區區幾行。 面試
//最簡單的建立多線程實例 編程
#include <stdio.h> windows
#include <windows.h> 安全
//子線程函數 多線程
DWORD WINAPI ThreadFun(LPVOID pM) dom
{ 函數
printf("子線程的線程ID號爲:%d\n子線程輸出Hello World\n", GetCurrentThreadId()); ui
return 0; this
}
//主函數,所謂主函數其實就是主線程執行的函數。
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,
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。不少運行庫中的函數在出錯時會將錯誤代號賦值給這個全局變量,這樣能夠方便調試。但若是有這樣的一個代碼片斷:
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._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系列函數》將爲你演示錯誤的結果(可能很是出人意料)並解釋產生這個結果的詳細緣由。