?
?? 使用多線程其實是非常容易的,下面這個程序的主線程會創建了一個子線程并等待其運行完畢,子線程就輸出它的線程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,
?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系列函數》將為你演示錯誤的結果(可能非常出人意料)并解釋產生這個結果的詳細原因。