Win32多线程编程(2) — 线程控制
Win32線(xiàn)程控制只有是圍繞線(xiàn)程這一內(nèi)核對(duì)象的創(chuàng)建、掛起、恢復(fù)、終結(jié)以及通信等操作,這些操作都依賴(lài)于Win32操作系統(tǒng)提供的一組API和具體編譯器的C運(yùn)行時(shí)庫(kù)函數(shù)。本篇圍繞這些操作接口介紹在Windows系統(tǒng)下的多線(xiàn)程編程要點(diǎn),后續(xù)將進(jìn)一步涉及多線(xiàn)程通信的同步互斥等議題。
?
1.線(xiàn)程的創(chuàng)建(CreateThread)
每個(gè)線(xiàn)程必須擁有一個(gè)進(jìn)入點(diǎn)函數(shù),線(xiàn)程從這個(gè)進(jìn)入點(diǎn)開(kāi)始運(yùn)行。主線(xiàn)程的進(jìn)入點(diǎn)是main/WinMain函數(shù),如果想在進(jìn)程中創(chuàng)建一個(gè)輔助線(xiàn)程,則必須為該輔助線(xiàn)程指定一個(gè)進(jìn)入點(diǎn)函數(shù),這個(gè)函數(shù)稱(chēng)為線(xiàn)程函數(shù)。
線(xiàn)程函數(shù)的定義如下:
typedef?DWORD?(WINAPI*?ThreadProc)(LPVOID?lpParam);?//?線(xiàn)程函數(shù)名稱(chēng)ThreadProc可以是任意的
WINAPI是一個(gè)宏名,在?windef.h文件中有如下的聲明。
#define?WINAPI?__stdcall
__stdcall?是Windows標(biāo)準(zhǔn)?C/C++函數(shù)的調(diào)用方法。從底層上說(shuō),使用這種調(diào)用方法參數(shù)的進(jìn)棧順序和標(biāo)準(zhǔn)?C調(diào)用(__cdecl?方法)是一樣的,都是從右到左,但是__stdcall?采用自動(dòng)清棧的方式,而__cdecl?采用的是手工清棧方式。
Windows?規(guī)定,凡是由它來(lái)負(fù)責(zé)調(diào)用的函數(shù)都必須定義為_(kāi)_stdcall?類(lèi)型。
ThreadProc是一個(gè)回調(diào)函數(shù),即由Windows系統(tǒng)來(lái)負(fù)責(zé)調(diào)用的函數(shù),所以此函數(shù)應(yīng)定義為_(kāi)_stdcall類(lèi)型。注意,如果沒(méi)有顯式說(shuō)明的話(huà),函數(shù)的調(diào)用方法是__cdecl。
Windows創(chuàng)建新線(xiàn)程的API是CreateThread,由該函數(shù)創(chuàng)建的線(xiàn)程將在調(diào)用者的虛擬地址空間內(nèi)執(zhí)行。函數(shù)原型如下:
// The CreateThread function creates a thread to execute within the virtual address space of the calling process.
// To create a thread that runs in the virtual address space of another process, use the CreateRemoteThread function.
HANDLE?CreateThread(
??????????????????LPSECURITY_ATTRIBUTES?lpThreadAttributes,?// SD
??????????????????SIZE_T?dwStackSize,???????????????????????// initial stack size
??????????????????LPTHREAD_START_ROUTINE?lpStartAddress,????// thread function
??????????????????LPVOID?lpParameter,???????????????????????// thread argument
??????????????????DWORD?dwCreationFlags,????????????????????// creation option
??????????????????LPDWORD?lpThreadId????????????????????????// thread identifier
??????????????????);
參數(shù)一為線(xiàn)程的安全屬性,一般設(shè)為NULL,表示使用默認(rèn)安全屬性。
參數(shù)二為線(xiàn)程堆棧大小,一般設(shè)為NULL,表示使用默認(rèn)堆棧大小,對(duì)應(yīng)VC的/STACK:鏈接器選項(xiàng)。VC6默認(rèn)的堆棧大小為1M,可通過(guò)“Project SettingsàLinkàStack allocations”設(shè)置堆棧大小;VC2005中,可在“項(xiàng)目屬性à配置屬性à鏈接器à系統(tǒng)”中設(shè)置堆棧大小。
參數(shù)三為線(xiàn)程函數(shù)的地址,傳遞函數(shù)指針或函數(shù)名ThreadProc。
參數(shù)四為傳遞給線(xiàn)程函數(shù)的參數(shù),即ThreadProc的參數(shù)。其為L(zhǎng)PVOID類(lèi)型,對(duì)復(fù)雜的參數(shù)采用結(jié)構(gòu)體或類(lèi)按址傳遞。
參數(shù)五為線(xiàn)程創(chuàng)建參數(shù),例如線(xiàn)程創(chuàng)建后是否立即啟動(dòng)的開(kāi)關(guān)選項(xiàng)。
參數(shù)六為內(nèi)核給新創(chuàng)建的線(xiàn)程分配的線(xiàn)程ID號(hào),為輸出參數(shù)。
用戶(hù)編寫(xiě)多線(xiàn)程程序時(shí),一般關(guān)注參數(shù)三和參數(shù)四足矣,其他可采用默認(rèn)參數(shù)。
此函數(shù)執(zhí)行成功后,將返回新建線(xiàn)程的線(xiàn)程句柄。lpStartAddress參數(shù)指定了線(xiàn)程函數(shù)的地址,新建線(xiàn)程將從此地址開(kāi)始執(zhí)行,直到?return?語(yǔ)句返回,線(xiàn)程運(yùn)行結(jié)束,把控制權(quán)交給操作系統(tǒng)。
線(xiàn)程內(nèi)核對(duì)象(kthread)
線(xiàn)程內(nèi)核對(duì)象就是一個(gè)包含了線(xiàn)程狀態(tài)信息的數(shù)據(jù)結(jié)構(gòu)。每一次對(duì)?CreateThread?函數(shù)的成功調(diào)用,系統(tǒng)都會(huì)在內(nèi)部為新的線(xiàn)程分配一個(gè)內(nèi)核對(duì)象。系統(tǒng)提供的管理線(xiàn)程的函數(shù)其實(shí)就是依靠訪(fǎng)問(wèn)線(xiàn)程內(nèi)核對(duì)象來(lái)實(shí)現(xiàn)管理的。在WinDbg中,可通過(guò)lkd> dt nt!_kthread查看線(xiàn)程內(nèi)核對(duì)象數(shù)據(jù)結(jié)構(gòu),這里涉及到線(xiàn)程上下文(Context)、使用計(jì)數(shù)(Usage Count)和暫停計(jì)數(shù)(Suspend Count)等重要概念。
(1)線(xiàn)程上下文
線(xiàn)程的上下文本質(zhì)上是一組處理器的寄存器,有正在執(zhí)行程序中的指針及堆棧指針。上下文及其轉(zhuǎn)換的過(guò)程根據(jù)處理器的結(jié)構(gòu)不同會(huì)有所不同,參考《線(xiàn)程的數(shù)據(jù)結(jié)構(gòu)》。在WinDbg中,可以通過(guò)lkd> dt nt!_context命令來(lái)觀察上下文的數(shù)據(jù)結(jié)構(gòu)。<WINNT.H>中定義了_CONTEXT結(jié)構(gòu)。
大約每經(jīng)?20ms,Windows?查看一次當(dāng)前存在的所有線(xiàn)程內(nèi)核對(duì)象。在這些對(duì)象中,只有一少部分是可調(diào)度的(沒(méi)有處于暫停狀態(tài)),Windows?選擇其中的一個(gè)內(nèi)核對(duì)象,將它的CONTEXT(上下文)裝入?CPU的寄存器,這一過(guò)程稱(chēng)為上下文切換。
用戶(hù)可調(diào)用GetThreadContext查看當(dāng)前線(xiàn)程的用戶(hù)模式的上下文信息;調(diào)用SetThreadContext改變線(xiàn)程上下文,待下次調(diào)度進(jìn)CPU時(shí)生效。其中,ContextFlags參數(shù)通過(guò)異或掩碼指定欲查看的寄存器。_KTHREAD::ContextSwitches為線(xiàn)程已切換的次數(shù)。
(2)使用計(jì)數(shù)
Usage Count成員記錄了線(xiàn)程內(nèi)核對(duì)象的使用計(jì)數(shù),這個(gè)計(jì)數(shù)說(shuō)明了此內(nèi)核對(duì)象被打開(kāi)的次數(shù)。線(xiàn)程內(nèi)核對(duì)象的存在與?Usage Count?的值息息相關(guān),當(dāng)這個(gè)值是0?的時(shí)候,系統(tǒng)就認(rèn)為已經(jīng)沒(méi)有任何進(jìn)程在引用此內(nèi)核對(duì)象了,于是線(xiàn)程內(nèi)核對(duì)象就要從內(nèi)存中撤銷(xiāo)。
只要線(xiàn)程沒(méi)有結(jié)束運(yùn)行,Usage??Count?的值就至少為?1。在創(chuàng)建一個(gè)新的線(xiàn)程時(shí),CreateThread?函數(shù)返回了線(xiàn)程內(nèi)核對(duì)象的句柄,相當(dāng)于打開(kāi)一次新創(chuàng)建的內(nèi)核對(duì)象,這也會(huì)促使?Usage Count?的值加1。所以創(chuàng)建一個(gè)新的線(xiàn)程后,初始狀態(tài)下?Usage Count?的值是2。之后,只要有進(jìn)程打開(kāi)此內(nèi)核對(duì)象,就會(huì)使Usage Count的值加1。比如當(dāng)有一個(gè)進(jìn)程調(diào)用OpenThread函數(shù)打開(kāi)這個(gè)線(xiàn)程內(nèi)核對(duì)象后,Usage Count?的值會(huì)再次加?1。
// The OpenThread function opens an existing thread object.
HANDLE?OpenThread(
????????????????DWORD?dwDesiredAccess,??// access right
????????????????BOOL?bInheritHandle,????// handle inheritance option
????????????????DWORD?dwThreadId????????// thread identifier
????????????????);
由于對(duì)這個(gè)函數(shù)的調(diào)用會(huì)使?Usage Count?的值加1,所以在使用完它們返回的句柄后一定要調(diào)用?CloseHandle?函數(shù)進(jìn)行關(guān)閉。關(guān)閉內(nèi)核對(duì)象句柄的操作就會(huì)使?Usage Count?的值減?1。
還有一些函數(shù)僅僅返回內(nèi)核對(duì)象的偽句柄,并不會(huì)創(chuàng)建新的句柄,當(dāng)然也就不會(huì)影響Usage Count?的值。如果對(duì)這些偽句柄調(diào)用?CloseHandle?函數(shù),那么?CloseHandle?就會(huì)忽略對(duì)自己的調(diào)用并返回?FALSE。對(duì)進(jìn)程和線(xiàn)程來(lái)說(shuō),這些函數(shù)有:
// The GetCurrentProcess function retrieves a pseudo handle for the current process.
HANDLE?GetCurrentProcess(VOID);
// The GetCurrentThread function retrieves a pseudo handle for the current thread.
HANDLE?GetCurrentThread(VOID);
前面提到,新創(chuàng)建的線(xiàn)程在初始狀態(tài)下?Usage??Count?的值是?2。此時(shí)如果立即調(diào)用CloseHandle?函數(shù)來(lái)關(guān)閉CreateThread返回的句柄的話(huà),Usage Count?的值將減為?1,但新創(chuàng)建的線(xiàn)程是不會(huì)被終止的。待線(xiàn)程函數(shù)返回,系統(tǒng)會(huì)使?Usage Count?的值由1減為0,線(xiàn)程的生命周期到此為止,系統(tǒng)將撤銷(xiāo)此線(xiàn)程內(nèi)核對(duì)象,釋放其所占內(nèi)存。
如果不關(guān)閉句柄的話(huà),Usage Count?的值將永遠(yuǎn)不會(huì)是?0,系統(tǒng)將永遠(yuǎn)不會(huì)撤銷(xiāo)它占用的內(nèi)存,這就會(huì)造成內(nèi)存泄漏(當(dāng)然,線(xiàn)程所在的進(jìn)程結(jié)束后,該進(jìn)程占用的所有資源都要釋放)。
(3)暫停計(jì)數(shù)
暫停計(jì)數(shù)參考下面的多線(xiàn)程狀態(tài)控制。
(4)主輔線(xiàn)程的執(zhí)行同步
一般主線(xiàn)程應(yīng)該后于輔助線(xiàn)程退出,如果主線(xiàn)程先退出,輔助線(xiàn)程尚在執(zhí)行,將會(huì)出現(xiàn)意想不到的結(jié)果,因此主線(xiàn)程必須對(duì)輔助線(xiàn)程具有完全的控制權(quán)。
一個(gè)可執(zhí)行對(duì)象有兩種狀態(tài),未受信(nonsignaled)和受信(signaled)狀態(tài)。線(xiàn)程內(nèi)核對(duì)象只有當(dāng)線(xiàn)程過(guò)程運(yùn)行結(jié)束時(shí)才達(dá)到受信狀態(tài)。可調(diào)用WaitForSingleObject/WaitForMultipleObjects函數(shù)在線(xiàn)程內(nèi)核對(duì)象(HANDLE)上等待,以便主輔線(xiàn)程同步。
?
2.線(xiàn)程的狀態(tài)控制(SuspendThread/ResumeThread、Sleep)
線(xiàn)程在創(chuàng)建后和終止前之間的狀態(tài),用戶(hù)可感知或控制的狀態(tài)主要有運(yùn)行和暫停(中斷)兩種,對(duì)應(yīng)的操作為掛起(Suspend)和恢復(fù)(Resume)。
線(xiàn)程內(nèi)核對(duì)象中的Suspend Count(_KTHREAD::SuspendCount)用于指明線(xiàn)程的暫停計(jì)數(shù)。
當(dāng)調(diào)用CreateProcess(創(chuàng)建進(jìn)程的主線(xiàn)程)或CreateThread函數(shù)時(shí),線(xiàn)程的內(nèi)核對(duì)象被創(chuàng)建了,它的暫停計(jì)數(shù)被初始化為1(即出于暫停狀態(tài)),這可以阻止新創(chuàng)建的線(xiàn)程被立即調(diào)度進(jìn)CPU中。因?yàn)榫€(xiàn)程初始化需要時(shí)間,當(dāng)線(xiàn)程完全初始化好了之后,CreateProcess或CreateThread檢查dwCreationFlags參數(shù)是否傳遞了CREATE_SUSPEND標(biāo)志,如果傳遞了,這些函數(shù)就返回,同時(shí)新線(xiàn)程處于暫停狀態(tài)。如果尚未傳遞該標(biāo)志,那么線(xiàn)程的暫停計(jì)數(shù)將被遞減為0。當(dāng)線(xiàn)程的暫停計(jì)數(shù)是?0的時(shí)候,該線(xiàn)程就進(jìn)入可調(diào)度狀態(tài)。
創(chuàng)建線(xiàn)程的時(shí)候,若指定CREATE_SUSPEND標(biāo)志,則用戶(hù)有機(jī)會(huì)在線(xiàn)程執(zhí)行任何代碼之前改變線(xiàn)程的運(yùn)行環(huán)境(如后面討論的優(yōu)先級(jí))。然后,需要調(diào)用ResumeThread函數(shù),減少線(xiàn)程的暫停計(jì)數(shù)至0,使線(xiàn)程恢復(fù)運(yùn)行,進(jìn)入可調(diào)度狀態(tài)。
// The ResumeThread function?decrements?a thread's suspend count. When the suspend count is decremented to zero, the execution of the thread is resumed.
DWORD?ResumeThread(
?????????????????HANDLE?hThread???// handle to thread
?????????????????);
后續(xù)可調(diào)用SuspendThread函數(shù)來(lái)暫停一個(gè)線(xiàn)程的運(yùn)行,因?yàn)樵揂PI傳遞的是句柄,故該函數(shù)可跨進(jìn)程調(diào)用,即在一個(gè)線(xiàn)程中暫停另一個(gè)線(xiàn)程。與ResumeThread相反,SuspendThread函數(shù)的調(diào)用會(huì)增加線(xiàn)程的暫停計(jì)數(shù)。
// The SuspendThread function suspends the specified thread.
DWORD?SuspendThread(
??????????????????HANDLE?hThread???// handle to thread
??????????????????);
只有當(dāng)線(xiàn)程的暫停計(jì)數(shù)是?0的時(shí)候,線(xiàn)程才能進(jìn)入可調(diào)度狀態(tài)。因此,必須注意SuspendThread/ResumeThread函數(shù)調(diào)用次數(shù)的匹配,以便正確控制。
SuspendThread/ResumeThread函數(shù)對(duì)于線(xiàn)程狀態(tài)的控制具有很強(qiáng)的針對(duì)性,另外一種簡(jiǎn)單的替代方案是調(diào)用Sleep函數(shù),讓調(diào)用線(xiàn)程睡一會(huì)兒,給其他的線(xiàn)程一個(gè)調(diào)度機(jī)會(huì),從而提供一種線(xiàn)程切換緩沖機(jī)制。
// The Sleep function suspends the execution of the current thread for the specified interval.
// To enter an alertable wait state, use the SleepEx function.
VOID?Sleep(
??????????DWORD?dwMilliseconds???// sleep time
??????????);
Sleep函數(shù)在線(xiàn)程的while(1)?死循環(huán)中非常實(shí)用,因?yàn)橐粋€(gè)線(xiàn)程如果while(1)輪回,則CPU使用率一般會(huì)飆升,置系統(tǒng)與卡死狀態(tài)。特殊地,Sleep(0)表示調(diào)用線(xiàn)程主動(dòng)放棄時(shí)間片的剩余部分,它強(qiáng)制系統(tǒng)調(diào)度其他線(xiàn)程。但是,系統(tǒng)有可能重新調(diào)度剛剛調(diào)用了Sleep的那個(gè)線(xiàn)程。?由于該調(diào)用本身占用時(shí)鐘周期,也會(huì)讓當(dāng)前線(xiàn)程放棄時(shí)間片,交出控制權(quán),從而使別的線(xiàn)程有機(jī)會(huì)執(zhí)行。當(dāng)然,可直接調(diào)用SwitchToThread()執(zhí)行線(xiàn)程切換。
?
3.線(xiàn)程的優(yōu)先級(jí)控制(GetThreadPriority/SetThreadPriority)
線(xiàn)程的狀態(tài)轉(zhuǎn)換、優(yōu)先級(jí)及調(diào)度方案,請(qǐng)參考《線(xiàn)程的調(diào)度》。
對(duì)于一個(gè)線(xiàn)程,我們可以調(diào)用GetThreadPriority函數(shù)來(lái)獲取其優(yōu)先級(jí)。
// The GetThreadPriority function retrieves the priority value for the specified thread. This value, together with the priority class of the thread's process, determines the thread's base-priority level.
int?GetThreadPriority(
????????????????????HANDLE?hThread???// handle to thread
????????????????????);
系統(tǒng)可以動(dòng)態(tài)地調(diào)整線(xiàn)程的優(yōu)先級(jí),當(dāng)系統(tǒng)希望這個(gè)線(xiàn)程處理窗口消息、I/O調(diào)用或是系統(tǒng)發(fā)現(xiàn)3-4秒內(nèi)這個(gè)線(xiàn)程一直迫切地需要一個(gè)時(shí)間片時(shí),它會(huì)將這個(gè)線(xiàn)程的優(yōu)先級(jí)調(diào)整為15并可以運(yùn)行雙倍的時(shí)間片。對(duì)線(xiàn)程優(yōu)先級(jí)的動(dòng)態(tài)調(diào)整是通過(guò)調(diào)用SetThreadPriority函數(shù)實(shí)現(xiàn)的。
// The SetThreadPriority function sets the priority value for the specified thread. This value, together with the priority class of the thread's process, determines the thread's base priority level.
BOOL?SetThreadPriority(
?????????????????????HANDLE?hThread,?// handle to the thread
?????????????????????int?nPriority???// thread priority level
?????????????????????);
????對(duì)于一般的應(yīng)用程序開(kāi)發(fā),按照常規(guī)的優(yōu)先級(jí)配置即可,很少涉及優(yōu)先級(jí)的操作。
?
4.線(xiàn)程的停止(ExitThread/TerminateThread)
線(xiàn)程過(guò)程的結(jié)束主要有兩種情形,即自然終止和人為中止。
自然終止,主要是指線(xiàn)程函數(shù)自然返回的情況,是壽終正寢的圓寂。人為中止是通過(guò)暴力手段將尚未完成使命的線(xiàn)程謀殺,是死于非命的夭折。
線(xiàn)程自然終止時(shí),會(huì)發(fā)生下列事件:
l??在線(xiàn)程函數(shù)中創(chuàng)建的所有?C++對(duì)象將通過(guò)它們各自的析構(gòu)函數(shù)被正確地銷(xiāo)毀。
l??該線(xiàn)程使用的堆棧將被釋放。
l??系統(tǒng)將線(xiàn)程內(nèi)核對(duì)象中?Exit Code(退出代碼)的值由?STILL_ACTIVE?設(shè)置為線(xiàn)程函數(shù)的返回值。
l??系統(tǒng)將遞減線(xiàn)程內(nèi)核對(duì)象中?Usage Code(使用計(jì)數(shù))的值。
線(xiàn)程結(jié)束后的退出代碼可以被其他線(xiàn)程用GetExitCodeThread函數(shù)檢測(cè)到,所以可以當(dāng)做自定義的返回值來(lái)表示線(xiàn)程的執(zhí)行結(jié)果。
人為中止主要有兩種手段:
(1)調(diào)用ExitThread(CRT中的exit)結(jié)束當(dāng)前線(xiàn)程(調(diào)用線(xiàn)程);
// The ExitThread function ends a thread.
VOID?ExitThread(
??????????????DWORD?dwExitCode???// exit code for this thread
??????????????);
ExitThread?函數(shù)會(huì)中止當(dāng)前線(xiàn)程的運(yùn)行,促使系統(tǒng)釋放掉所有此線(xiàn)程使用的資源。但是,線(xiàn)程函數(shù)中申請(qǐng)的C++資源,典型的如C++類(lèi)(class)卻不能得到正確地清除。這是因?yàn)镃++對(duì)象的析構(gòu)函數(shù)是通過(guò)atexit掛接到線(xiàn)程中,在exit退出時(shí)(main/WinMain或線(xiàn)程過(guò)程返回之后)調(diào)用。若線(xiàn)程過(guò)程被終止,則CRT越過(guò)atexit,從而使C++資源不能正確釋放。所以,結(jié)束線(xiàn)程最好的方法還是讓線(xiàn)程自然返回。
(2)調(diào)用TerminateThread進(jìn)行跨線(xiàn)程中止。
// The TerminateThread function terminates a thread.
BOOL?TerminateThread(
???????????????????HANDLE?hThread,????// handle to thread
???????????????????DWORD?dwExitCode???// exit code
???????????????????);
因?yàn)樵揂PI傳遞的是句柄,故該函數(shù)可跨線(xiàn)程調(diào)用,即在一個(gè)線(xiàn)程中中止另一個(gè)線(xiàn)程。
這是一個(gè)被強(qiáng)烈建議避免使用的函數(shù),因?yàn)橐坏﹫?zhí)行這個(gè)函數(shù),程序無(wú)法預(yù)測(cè)目標(biāo)線(xiàn)程會(huì)在何處被中止,其結(jié)果就是目標(biāo)線(xiàn)程可能根本沒(méi)有機(jī)會(huì)來(lái)做清除工作,如線(xiàn)程中打開(kāi)的文件和申請(qǐng)的內(nèi)存都不會(huì)被釋放。另外,使用TerminateThread?函數(shù)中止線(xiàn)程的時(shí)候,系統(tǒng)不會(huì)釋放線(xiàn)程使用的堆棧。所以建議讀者在編程的時(shí)候盡量讓線(xiàn)程自己退出,如果主線(xiàn)程要求某個(gè)線(xiàn)程結(jié)束,可以通過(guò)各種方法通知線(xiàn)程,線(xiàn)程收到通知后自行退出。只有在迫不得已的情況下,才使用?TerminateThread?函數(shù)終止線(xiàn)程。
無(wú)論是自然終止還是人為中止,它們都將使使用計(jì)數(shù)減1。此時(shí),我們通過(guò)調(diào)用GetExitCodeThread函數(shù)來(lái)獲取線(xiàn)程函數(shù)的返回值。最后,必須調(diào)用CloseHandle關(guān)閉線(xiàn)程句柄,使使用計(jì)數(shù)再減1。至此,該線(xiàn)程內(nèi)核對(duì)象結(jié)束生命的旅程。
總之,始終應(yīng)該讓線(xiàn)程正常退出,即使它的線(xiàn)程函數(shù)自然返回。通知線(xiàn)程退出的方法很多,如設(shè)置全局變量、使用事件對(duì)象等,這涉及到下一節(jié)線(xiàn)程間的通信問(wèn)題。
?
5.用CRT的_beginthreadex代替操作系統(tǒng)的CreateThread
(1)為IDE選擇正確的RTL(Run Time Library)
在VC6中,“Project Settings?à?C/C++?à?Category(Code Generation)à?Use run-time library”共有六個(gè)選項(xiàng)供選擇:
/ML:Single-Threaded*
/MLd:Debug Single-Threaded
/MT:Multithreaded
/MTd:Debug Multithreaded
/MD:Multithreaded DLL
/MDd:Debug Multithreaded DLL
其中/ML[d]和/MT[d]主要針對(duì)常規(guī)多線(xiàn)程應(yīng)用程序的RTL版本選擇,/MD[d]主要針對(duì)多線(xiàn)程LIB或DLL工程中的RTL版本選擇。
VC6常規(guī)工程的默認(rèn)選項(xiàng)為/ML[d],MFC工程的默認(rèn)選項(xiàng)為/MD[d]。對(duì)于需要多線(xiàn)程支持的工程中,如果不設(shè)定/MT或/MD選項(xiàng),則可能出現(xiàn)“error LNK2001: unresolved external symbol __beginthreadex”或“error C2065: '_beginthreadex' : undeclared identifier”。
在VC2005中,“項(xiàng)目屬性?à?配置屬性?à?C/C++??à?代碼生成”中提供了四種選擇,
多線(xiàn)程(/MT)
多線(xiàn)程調(diào)試(/MTd)
多線(xiàn)程DLL(/MD)
多線(xiàn)程調(diào)試DLL(/MDd)
VC2005工程默認(rèn)選項(xiàng)為/MD[d]。
不同的鏈接選項(xiàng),鏈接器將選擇不同的鏈接庫(kù)進(jìn)行鏈接。參考《C Run-Time Libraries (CRT)》和《/MD, /ML, /MT, /LD???(Use Run-Time Library)》。
(2)用C運(yùn)行時(shí)庫(kù)的_beginthreadex代替操作系統(tǒng)的CreateThread來(lái)創(chuàng)建線(xiàn)程
在實(shí)際的開(kāi)發(fā)過(guò)程中,一般不直接使用Windows系統(tǒng)提供的CreateThread函數(shù)創(chuàng)建線(xiàn)程,而是使用?C/C++運(yùn)行期函數(shù)_beginthread(ex)/_endthread(ex)。
使用_beginthread無(wú)法創(chuàng)建帶有安全屬性的新線(xiàn)程,無(wú)法創(chuàng)建暫停的線(xiàn)程,也無(wú)法獲得線(xiàn)程ID。_endthread的情況類(lèi)似,它不帶參數(shù),這意味這線(xiàn)程的退出代碼必須硬編碼為0。一般不調(diào)用_beginthread/_endthread,而是調(diào)用其擴(kuò)展版本_beginthreadex/_endthreadex。
事實(shí)上,C/C++運(yùn)行期庫(kù)提供CreateThread加強(qiáng)版的_beginthreadex,是為了多線(xiàn)程同步的需要。在早期的單線(xiàn)程C運(yùn)行庫(kù)中有許多的全局變量,如errno、strerror等,它們可以用來(lái)表示線(xiàn)程當(dāng)前的一些狀態(tài)。
但是在多線(xiàn)程程序設(shè)計(jì)中,每個(gè)線(xiàn)程必須有惟一的狀態(tài),否則這些變量記錄的信息就不會(huì)準(zhǔn)確了。比如,全局變量errno?用于表示調(diào)用運(yùn)行期函數(shù)失敗后的錯(cuò)誤代碼。如果所有線(xiàn)程共享一個(gè)errno?的話(huà),在一個(gè)線(xiàn)程產(chǎn)生的錯(cuò)誤代碼就會(huì)影響到另一個(gè)線(xiàn)程。為了解決這個(gè)問(wèn)題,每個(gè)線(xiàn)程都需要有自己的errno?變量。
要想使運(yùn)行期為每個(gè)線(xiàn)程都設(shè)置狀態(tài)變量,必須在創(chuàng)建線(xiàn)程的時(shí)候調(diào)用運(yùn)行期提供的_beginthreadex,讓運(yùn)行期設(shè)置了相關(guān)變量后再去調(diào)用Windows系統(tǒng)提供的?CreateThread函數(shù)。
_beginthreadex的參數(shù)與CreateThread函數(shù)對(duì)應(yīng),函數(shù)的參數(shù)和數(shù)據(jù)類(lèi)型都是C Run-time Library中的類(lèi)型,在使用時(shí)候需要強(qiáng)制類(lèi)型轉(zhuǎn)換。
// /Microsoft Visual Studio/VC98/CRT/SRC/THREADEX.C
unsigned long?__cdecl?_beginthreadex(
????????void *security,
????????unsigned?stacksize,
????????unsigned (__stdcall*?initialcode)(void*),
????????void *?argument,
????????unsigned?createflag,
????????unsigned *thrdaddr)
{
_ptiddata?ptd;??????????????????/* pointer to per-thread data */
// ……
ptd->_initaddr?= (void *)initialcode;
ptd->_initarg?=?argument;
ptd->_thandle?= (unsigned long)(-1L);
// ……
CreateThread(security,
???????stacksize,
???????_threadstartex,
???????(LPVOID)ptd,??// pointer to per-thread data(_ptiddata?ptd)
???????createflag,
???????thrdaddr));
????// ……
}
線(xiàn)程啟動(dòng)函數(shù)為_threadstartex,其參數(shù)為線(xiàn)程局部存儲(chǔ)(TLS)結(jié)構(gòu)_ptiddata?ptd,其中包含了線(xiàn)程函數(shù)ptd->_initaddr和線(xiàn)程參數(shù)ptd->_initarg。關(guān)于線(xiàn)程局部存儲(chǔ)(TLS),參考后續(xù)議題。實(shí)際上_ptiddata結(jié)構(gòu)中定義了_terrno、_token、_errmsg等單線(xiàn)程全局變量。
struct?_tiddata
{
????unsigned long???_tid;???????????/* thread ID */
????unsigned long???_thandle;???????/* thread handle */
???
????int?????_terrno;????????????????/* errno value */
unsigned long???_holdrand;??????/* rand() seed value */
char?*??????_token;?????????????/* ptr to strtok() token */
?
char?*??????_errmsg;????????????/* ptr to strerror()/_strerror() buff */
?
????void?*??????_initaddr;??????????/* initial user thread address */
void?*??????_initarg;???????????/* initial user thread argument */
}
typedef struct?_tiddata?*?_ptiddata;
// _threadstartex() - New thread begins here
static unsigned long?WINAPI?_threadstartex(void *ptd)
{
// ……
// Call fp initialization, if necessary
if (?_FPmtinit?!=?NULL?)
(*_FPmtinit)();
// ……
_endthreadex(((unsigned (WINAPI?*)(void*))(((_ptiddata)ptd)->_initaddr))(((_ptiddata)ptd)->_initarg));
}
線(xiàn)程啟動(dòng)函數(shù)_threadstartex中,進(jìn)行相關(guān)的初始化(_FPmtinit)后,開(kāi)始調(diào)用ptd->_initaddr(ptd->_initarg)執(zhí)行真正的線(xiàn)程過(guò)程。傳遞線(xiàn)程過(guò)程返回碼調(diào)用_endthreadex函數(shù)。
// _endthreadex() - Terminate the calling thread
void?__cdecl?_endthreadex(unsigned?retcode)
_endthreadex函數(shù)釋放線(xiàn)程局部存儲(chǔ)(TLS)數(shù)據(jù)_ptiddata?ptd,調(diào)用ExitThread結(jié)束當(dāng)前線(xiàn)程的運(yùn)行。故這里有了不調(diào)用ExitThread的另一個(gè)理由:即它會(huì)阻止線(xiàn)程的_ptiddata內(nèi)存的釋放。故建議使用_endthreadex替代ExitThread調(diào)用,當(dāng)然,這也是不應(yīng)該提倡的做法。
參考《_beginthreadex和CreateThread》。
總結(jié)
以上是生活随笔為你收集整理的Win32多线程编程(2) — 线程控制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Win32多线程编程(1) — 基础概念
- 下一篇: Win32多线程编程(3) — 线程同步