在Windows的多線程編程中,創建線程的函數主要有CreateThread,_beginthead(_beginthreadex)和AfxBeginThread,那么它們之間有什么聯系與區別呢?當我需要創建一個線程時該用哪個函數呢?
下面先介紹各個函數的用法:
CreateThread:
函數原型:
- HANDLE WINAPI CreateThread(
- _in LPSECURITY_ATTRIBUTES lpThreadAttributes,
- _in SIZE_T dwStackSize,
- _in LPTHREAD_START_ROUTINE lpStartAddress,
- _in LPVOID lpParameter,
- _in DWORD dwCreationFlags,
- _out LPDWORD lpThreadId
- );
參數:
lpThreadAttributes: 指向一個LPSECURITY_ATTRIBUTES結構的指針決定返回的句柄能否被繼承,如果lpThreadAttributes為空,這個句柄不能被繼承。
sdStackSize:初始化的堆棧大小,以字節為單位。如果為0,使用默認的大小。
lpStartAddress:函數的入口地址,是一個函數指針。
lpParameter:一個指針,被傳遞到線程函數里。
dwCreationFlags:線程創建的標志。如果為CREATE_SUSPENDED這個標志,那么需要使用ResumeThread函數來激活線程函數,如果為NULL,線程函數立刻執行。
IpThreadId:一個指向線程id的指針,如果為空,線程id不被返回。
返回值:
1:如果函數成功執行,返回值將是這個新線程的句柄。如果失敗,返回值是NULL。
2:當線程函數的起始地址無效(或者不可訪問)時,CreateThread函數仍可能成功返回。如果該起始地址無效,則當線程運行時,異常將發生,線程終止,並返回一個錯誤代碼,可以使用GetLastError獲取。
說明:
1:如果線程函數return,返回值會隱式條用ExitThread函數,可以使用GetExitCodeThread函數獲得該線程函數的返回值。
2:使用CreateThread創建的線程具有THREAD_PRIORITY_NORMAL線程優先級。可以使用GetThreadPriority和SetThreadPriority函數獲取和設置線程優先級值。
3:當一個線程結束時,這個線程的對象將獲得有信號狀態,使得任何等待這個對象的線程都能夠成功並繼續執行下去。
4:系統中的線程對象一直存活到線程結束,並且所有指向它的句柄都需要通過調用CloseHandle關閉。
5:如果一個線程調用了CRT,應該使用_beginthreadex 和_endthreadex(需要使用多線程版的CRT)。
_beginthread與_beginthreadex:
函數原型:
- uintptr_t _beginthread(
- void( *start_address )( void * ),
- unsigned stack_size,
- void *arglist
- );
- uintptr_t _beginthreadex(
- void *security,
- unsigned stack_size,
- unsigned ( *start_address )( void * ),
- void *arglist,
- unsigned initflag,
- unsigned *thrdaddr
- );
參數:
start_address:線程函數的入口地址。對於_beginthread,線程函數的調用約定是_cdecl。對於_beginthreadex,線程函數的調用約定是_stdcall。
stack_size:線程堆棧大小,可以為0。
arglist:傳遞給線程函數的參數,可以為0。
security:線程安全屬性。
initflag:線程創建的初始標志。為CREATE_SUSPENDED則掛起線程,使用ResumeThread激活線程,為NULL則立即執行。
thrdaddr:線程Id。
返回值:
1:如果成功,將會返回一個新的線程句柄。然而,如果線程函數執行的很快,_beginthread可能得到一個非法的句柄。
2:如果失敗,_beginthread返回-1,此時errno變量將被設置。_beginthreadex返回0,此時errno和_doserrno都被設置。
說明:
1:_beginthread函數的線程入口函數必須使用_cdecl調用約定。_beginthreadex函數的線程入口函數必須使用_stdcall調用約定
2:使用_beginthreadex比使用_begingthread更加安全。因為_beginthread的線程函數可能執行很快,這時可能會返回一個非法的句柄。
3:_endthread將會自動的關閉線程句柄,然而_beginthreadex不會,需要使用CloseHandle現實的關閉句柄。所以_beginthreadex函數可以使用WaitForSingleObject函數來獲取線程對象來進行同步。
4:一個連接Libcmt.lib的可執行文件,不要調用ExitThread函數,這個函數會阻止系統的運行時回收已分配的資源。使用_endthread and _endthreadex可以回收已分配的資源然后再調用ExitThread.
5: 可以調用_endthread和_endthreadex顯示式結束一個線程。然而,當線程函數返回時,_endthread和_endthreadex 被自動調用。endthread和_endthreadex的調用有助於確保分配給線程的資源的合理回收。
6:當_beginthread和_beginthreadex被調用時,操作系統自己處理線程棧的分配。如果在調用這些函數時,指定棧大小為0,則操作系統 為該線程創建和主線程大小一樣的棧。如果任何一個線程調用了abort、exit或者ExitProcess,則所有線程都將被終止。
7:對於使用C運行時庫里的函數的線程應該使用_beginthread和_endthread這些C運行時函數來管理線程,而不是使用CreateThread和ExitThread。否則,當調用ExitThread后,可能引發內存泄露。
8:必須使用多線程版的 C run_time libraries.
AfxBeginThread:
函數原型:
- CWinThread* AfxBeginThread(
- AFX_THREADPROC pfnThreadProc,
- LPVOID pParam,
- int nPriority = THREAD_PRIORITY_NORMAL,
- UINT nStackSize = 0,
- DWORD dwCreateFlags = 0,
- LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
- );
- CWinThread* AfxBeginThread(
- CRuntimeClass* pThreadClass,
- int nPriority = THREAD_PRIORITY_NORMAL,
- UINT nStackSize = 0,
- DWORD dwCreateFlags = 0,
- LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
- );
參數:
pfnThreadProc:線程函數的入口地址。
函數原型:UINT __cdecl MyControllingFunction( LPVOID pParam );
pThreadClass:繼承CWinThread類的RUNTIME_CLASS對象。
pParam: 傳遞給線程函數的參數,可以為0。
nPriority:線程優先級。
nStackSize:指明線程堆棧的大小,以字節為單位,可以為0。
dwCreateFlags:線程創建標志。
lpSecurityAttrs:線程安全屬性。
返回值:
如果成功則返回一個指針指向線程對象,否則為NULL。
說明:
可以調用AfxEndThread來終止線程或者return。
下面來介紹下這幾個函數的聯系與區別:
CreateThread:
CreateThread是Windows的API函數,提供操作系統級別的創建線程的操作。_beginthread(及_beginthreadex)與AfxBeginThread的底層實現都調用了CreateThread函數。
CreateThread函數沒有考慮到下面二點:
(1)C Runtime中需要對多線程進行記錄和初始化,以保證C函數庫工作正常(典型的例子就是strtok函數)
(2)MFC也需要知道新線程的創建,也需要做一些初始化工作。
所以,在不調用MFC和CRT的函數時,可以用CreateThread創建線程,其它情況不要使用。
AfxBeginThread:
MFC中線程創建的函數,首先創建了相應的CWinThread對象,然后調用CWinThread::CreateThread,在CWinThread::CreateThread中完成了對線程對象的初始化工作,然后調用_beginthreadex創建線程。注意不要在一個MFC程序中使用_beginthreadex()或CreateThread()。
_beginthread和_beginthreadex: (實現文件分別是thread.c和threadex.c)
是MS對C Runtime庫的擴展SDK函數,首先對C Runtime庫做了一些初始化的工作,以保證C Runtime庫工作正常。然后,調用CreateThread真正創建線程。
若要使多線程C和C++程序能夠正確地運行,必須創建一個數據結構,並將它與使用C/C++運行期庫函數的每個線程關聯起來。當你調用C/C++運行期庫時,這些函數必須知道查看調用線程的數據塊,這樣就不會對別的線程產生不良影響。
1.每個線程均獲得由C/C++運行期庫的堆棧分配的自己的tiddata內存結構。
2.傳遞給_beginthreadex的線程函數的地址保存在tiddata內存塊中。傳遞給該函數的參數也保存在該數據塊中。(指向tiddata結構的指針會作為一個TLS保存起來)
3._beginthreadex確實從內部調用CreateThread,因為這是操作系統了解如何創建新線程的唯一方法。
4.當調用CreatetThread時,它被告知通過調用_threadstartex而不是pfnStartAddr來啟動執行新線程。還有,傳遞給線程函數的參數是tiddata結構而不是pvParam的地址。
5.如果一切順利,就會像CreateThread那樣返回線程句柄。如果任何操作失敗了,便返回 NULL。
總結:
1:CreateThread是由操作系統提供的接口,而AfxBeginThread和_BeginThread則是編譯器對它的封裝。
2:用_beginthreadex()、_endthreadex函數應該是最佳選擇,且都是C Run-time Library中的函數,函數的參數和數據類型都是C Run-time Library中的類型,這樣在啟動線程時就不需要進行Windows數據類型和C Run-time Library中的數據類型之間的轉化,從而減低了線程啟動時的資源消耗和時間的消耗。
3:MFC也是C++類庫(只不過是Microsoft的C++類庫,不是標准的C++類庫),在MFC中也封裝了new和delete兩中運算符,所以用到new和delete的地方不一定非要使用_beginthreadex() 函數,用其他兩個函數都可以。
4:_beginthreadex和_beginthread在回調入口函數之前進行一些線程相關的CRT的初始化操作。CRT的函數庫在線程出現之前就已經存在,所以原有的CRT不能真正支持線程,這也導致了許多CRT的函數在多線程的情況下必須有特殊的支持,不能簡單的使CreateThread就可以。
5:如果要作多線程(非MFC)程序,在主線程以外的任何線程內
使用malloc(),free(),new
調用stdio.h或io.h,包括fopen(),open(),getchar(),write(),printf(),errno
使用浮點變量和浮點運算函數
調用那些使用靜態緩沖區的函數如: asctime(),strtok(),rand()等。
應該使用多線程的CRT並配合_beginthreadex(該函數只存在於多線程CRT), 其他情況,你可以使用單線程的CRT並配合CreateThread。因為對產生的線程而言,_beginthreadex比CreateThread會為上述操作多做額外的工作,比如幫助strtok()為每個線程准備一份緩沖區。
然而多線程程序極少情況不使用上述那些函數(比如內存分配或者io),所以與其每次都要思考是要使用_beginthreadex還是CreateThread,不如就一棍子敲定_beginthreadex。
6:你也許會借助win32來處理內存分配和io,這時候你確實可以以單線程crt配合CreateThread,因為io的重任已經從crt轉交給了win32。這時通常你應該使用HeapAlloc,HeapFree來處理內存分配,用CreateFile或者GetStdHandle來處理io。
7:還有一點比較重要的是_beginthreadex傳回的雖然是個unsigned long,其實是個線程Handle(事實上_beginthreadex在內部就是調用了CreateThread),所以你應該用CloseHandle來結束他。千萬不要使用ExitThread()來退出_beginthreadex創建的線程,那樣會喪失釋放簿記數據的機會,應該使用_endthreadex.
下面對兩個概念進行闡述
CRT(C/C++ Runtime Library):
是一種函數庫,由編譯器的生產廠家提供頭文件或接口,操作系統提供運行時庫的實現。所以Windows和Linux系統的運行時庫函數接口雖然一樣,但具體實現不一樣。
CRT是支持C/C++運行的一系列函數和代碼的總稱,雖然沒有一個很精確的定義,但是可以知道,你的main函數就是它負責調用的,還有平時使用的strlen,strtok,time,atoi之類的函數也是它提供的。
線程局部存儲(TLS,thread local storage)
一個多線程程序中,全局變量(及分配的內存)被所有線程所共享。函數的靜態局部變量也被所有使用該函數的線程所共享。一個函數中的自動變量對每一個線程是唯一的,因為它們存儲於堆棧上,而每個線程都有他們自己的堆棧。有時,我們需要對每一個線程唯一的持續性存儲。例如,C函數strtok就需要這種存儲。不幸的是,C語言不支持這種變量。但是Windows提供了四個API函數來實現這種機制。我們把這種存儲稱為線程局部存儲(TLS,Thread Local Storage)。
首先,定義一個結構,把對每個線程唯一的數據包含在該結構中。
例如:
typedef struct
{
int one;
int two;
} DATA, *PDATA;
然后,主線程調用TlsAlloc函數來為進程獲得一個TLS索引:tlsIndex = TlsAlloc();該TLS索引可以存儲於一個全局變量或者通過線程函數的參數傳遞給其它線程。每個需要使用該TLS索引的線程,先動態分配內存,然后調用TlsSetValue函數將該內存關聯到該TLS索引(及該線程): TlsSetValue(tlsIndex, GlobalAlloc(GPTR, sizeof(DATA));
此時,線程直接或間接調用的函數可以通過如下方式獲得該線程的TLS存儲區域:
PDATA pdata;
pdata = (PDATA) TlsGetValue(tlsIndex);
此時,就可以使用該線程的TLS存儲區的變量了。
當線程函數終止時,它應該釋放它所分配的動態空間: GlobalFree(TlsGetValue(tlsIndex));
當所有使用TLS的線程都終止后,主線程應當釋放該TLS存儲空間: TlsFree(tlsIndex);
TLS可以以一種更簡單的方式使用,那就是通過Winodws對C所作的擴展關鍵字__declspec和擴展存儲類型修飾符thread。例如:
__declspec(thread) int global_tls_i = 1; // 在函數外部,聲明一個TLS變量
__declspec(thread) static int local_tls_i = 2; // 在函數內部聲明一個靜態TLS變量