標籤:指標 子線程 代號 roc ... art sse main efi
轉載 MoreWindows: 秒殺多線程第二篇
本文將帶領你與多線程作第一次親密接觸,並深入分析 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
函數功能:建立線程
MSDN中CreateThread原型:
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,//SDSIZE_T dwStackSize,//initialstacksizeLPTHREAD_START_ROUTINE lpStartAddress,//threadfunctionLPVOID lpParameter,//threadargumentDWORD dwCreationFlags,//creationoptionLPDWORD lpThreadId//threadidentifier)//lpThreadAttributes:指向SECURITY_ATTRIBUTES型態的結構的指標。在Windows 98中忽略該參數。在Windows NT中,NULL使用預設安全性,不可以被子線程繼承,否則需要定義一個結構體將它的bInheritHandle成員初始化為TRUE.//dwStackSize,設定初始棧的大小,以位元組為單位,如果為0,那麼預設將使用與調用該函數的線程相同的棧空間大小。任何情況下,Windows根據需要動態延長堆棧的大小。//lpStartAddress,指向線程函數的指標,形式:@函數名,函數名稱沒有限制,但是必須以下列形式聲明://DWORD WINAPI 函數名(LPVOID lpParam),格式不正確將無法調用成功。//lpParameter:向線程函數傳遞的參數,是一個指向結構的指標,不需傳遞參數時,為NULL。//dwCreationFlags :線程標誌 等於0時表示建立後立即啟用。//lpThreadId: 儲存新線程的id。若不想返回線程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; }
[c++] 多線程