第二章 線程的第一次接觸
資源網(wǎng)絡(luò)收集 感謝原創(chuàng)者
轉(zhuǎn)自http://blog.sina.com.cn/s/blog_5678943c0100d4po.html
本章回答了如下幾個(gè)問題:
◆ 怎樣建立一個(gè)線程?怎樣終止一個(gè)線程?線程的退出碼如何獲取?
◆ 使用多線程容易引起怎樣的問題?如何解決?
◆ 什么是worker線程?什么是GDI線程?它們的區(qū)別何在?程序處理上有何不同?各需注意些什么?
建立線程序
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security attributes
DWORD dwStackSize, // initial thread stack size
LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function
LPVOID lpParameter, // argument for new thread
DWORD dwCreationFlags, // creation flags
LPDWORD lpThreadId // pointer to receive thread ID
);
調(diào)用約定
調(diào)用約定定義函數(shù)調(diào)用時(shí)參數(shù)傳遞的方式、堆棧內(nèi)參數(shù)的處理等。
通常使用關(guān)鍵字__stdcall、__cdecl和__fastcall直接加函數(shù)前預(yù)以明確。
#define WINAPI __stdcall
__stdcall是Pascal程序的缺省調(diào)用方式,通常用于Win32 API中,函數(shù)采用從右到左的壓棧方式,自己在退出時(shí)清空堆棧。
C調(diào)用約定(即用__cdecl關(guān)鍵字說明)按從右至左的順序壓參數(shù)入棧,由調(diào)用者把參數(shù)彈出棧。對(duì)于傳送參數(shù)的內(nèi)存棧是由調(diào)用者來維護(hù)的(正因?yàn)槿绱耍瑢?shí)現(xiàn)可變參數(shù)vararg的函數(shù)(如printf)只能使用該調(diào)用約定)。
__cdecl是C和C++程序的缺省調(diào)用方式。每一個(gè)調(diào)用它的函數(shù)都包含清空堆棧的代碼,所以產(chǎn)生的可執(zhí)行文件大小會(huì)比調(diào)用_stdcall函數(shù)的大。_cdecl是MFC缺省調(diào)用約定。
__fastcall調(diào)用的主要特點(diǎn)就是快,因?yàn)樗峭ㄟ^寄存器來傳送參數(shù)的(實(shí)際上,它用ECX和EDX傳送前兩個(gè)雙字(DWORD)或更小的參數(shù),剩下的參數(shù)仍舊自右向左壓棧傳送,被調(diào)用的函數(shù)在返回前清理傳送參數(shù)的內(nèi)存棧。
thiscall僅僅應(yīng)用于“C++”成員函數(shù)。this指針存放于CX/ECX寄存器中,參數(shù)從右到左壓。thiscall不是關(guān)鍵詞,因此不能被程序員指定。
關(guān)鍵字__stdcall、__cdecl和__fastcall可以直接加在要輸出的函數(shù)前。它們對(duì)應(yīng)的命令行參數(shù)分別為/Gz、/Gd和/Gr。缺省狀態(tài)為/Gd,即__cdecl。
要完全模仿PASCAL調(diào)用約定首先必須使用__stdcall調(diào)用約定,至于函數(shù)名修飾約定,可以通過其它方法模仿。還有一個(gè)值得一提的是WINAPI宏,Windows.h支持該宏,它可以將出函數(shù)翻譯成適當(dāng)?shù)恼{(diào)用約定,在WIN32中,它被定義為__stdcall。使用WINAPI宏可以創(chuàng)建自己的APIs。
幾個(gè)必須牢記心頭的概念
線程之間的執(zhí)行次序應(yīng)該視之為隨機(jī);
任務(wù)切換可能在任何時(shí)刻任何地點(diǎn)發(fā)生;
線程并不總是立刻啟動(dòng)(即使進(jìn)程創(chuàng)建時(shí)并未設(shè)置CREATE_SUSPENDED標(biāo)志)。
核心對(duì)象(Kernal Objects)
CreateThread()傳回兩個(gè)值,用以標(biāo)識(shí)一個(gè)新線程。一個(gè)是線程句柄,一個(gè)是線程ID。線程ID是一個(gè)全局變量,可以獨(dú)一無二地表示系統(tǒng)任一進(jìn)程中的某個(gè)線程。 AttachThreadInput()和PostThreadMessage()就需要用到線程ID,這兩個(gè)函數(shù)允許你影響其他線程的消息隊(duì)列。調(diào)試器和進(jìn)程觀察器也需要線程ID。
為了安全防護(hù)的緣故,不可能根據(jù)一個(gè)線程ID而獲得其句柄。
所謂handle,其實(shí)是個(gè)指針,指向操作系統(tǒng)內(nèi)存中的某樣?xùn)|西。為了維護(hù)系統(tǒng)的完整性與安全性,那東西不允許你直接取得。
Win32核心對(duì)象清單:
進(jìn)程(processes)
線程(threads)
文件(files)
事件(events)
信號(hào)量(semaphores)
互斥器(mutexes)
管道(pipes。分為named和anonymous兩種)
注意臨界區(qū)不是核心對(duì)象!
核心對(duì)象和GDI對(duì)象
核心對(duì)象由KERNEL32.DLL管理,GDI對(duì)象由GDI32.DLL管理。
GDI對(duì)象是Windows的基礎(chǔ)部分。在Win16和Win32中它們都是由操作系統(tǒng)管理。通常你不需要知道其數(shù)據(jù)格式。Windows隱藏了實(shí)現(xiàn)細(xì)節(jié),只是給你一個(gè)對(duì)象句柄。
GDI對(duì)象和核心對(duì)象之間有一個(gè)主要的不同。GDI對(duì)象有單一擁有者,不是進(jìn)程就是線程。核心對(duì)象可以有一個(gè)以上的擁有者,甚至可以跨進(jìn)程。
為了保持對(duì)每一位主人的追蹤,核心對(duì)象保持了一個(gè)引用計(jì)數(shù)(reference count),以記錄有多少handles對(duì)應(yīng)到此對(duì)象。對(duì)象中也記錄了哪一個(gè)進(jìn)程或線程是擁有者。如果你調(diào)用CreateThread()或是其他會(huì)傳回handle的函數(shù),引用計(jì)數(shù)便加1。當(dāng)你調(diào)用CloseHandle()時(shí),引用計(jì)數(shù)便減1。一旦引用計(jì)數(shù)降至0,這一核心對(duì)象便自動(dòng)銷毀。
由于引用計(jì)數(shù)的設(shè)計(jì),對(duì)象有可能在“產(chǎn)生該對(duì)象之進(jìn)程”結(jié)束之后還繼續(xù)幸存(比如用于進(jìn)程間通訊的事件對(duì)象、信號(hào)量等)。Win32提供各種機(jī)制,讓其他進(jìn)程得以取得一個(gè)核心對(duì)象的句柄,如果某個(gè)進(jìn)程握有某個(gè)核心對(duì)象的句柄,而該對(duì)象的原創(chuàng)者(進(jìn)程)已經(jīng)“作古”了,此核心對(duì)象并不會(huì)被摧毀。
為什么我應(yīng)該調(diào)用CloseHandle()?
如果進(jìn)程結(jié)束之前沒有對(duì)它所打開的核心對(duì)象調(diào)用CloseHandle(),操作系統(tǒng)會(huì)自動(dòng)地把那些對(duì)象的應(yīng)用計(jì)數(shù)減一。雖然可以依賴操作系統(tǒng)作實(shí)體(physical)上的清除(cleanup)工作,然后邏輯上的清除操作不是同一回事,特別是你有許多進(jìn)程的話。
如果一個(gè)進(jìn)程常常產(chǎn)生工作線程(worker thread)而老不關(guān)閉線程的句柄,那么這個(gè)進(jìn)程將有許許多多的線程核心對(duì)象留給操作系統(tǒng)去清理。這樣的資源泄漏(resource leak)可能會(huì)對(duì)效率帶來負(fù)面影響。
心得:CloseHandle()實(shí)際上進(jìn)行的是邏輯清除。盡管操作系統(tǒng)會(huì)幫我們物理清除,但只有當(dāng)進(jìn)程執(zhí)行完畢才可以,而且操作系統(tǒng)并不能確切地知道這些核心對(duì)象的具體含義,無法知道它們的解構(gòu)的次序,因此可能會(huì)造成一些不期待的問題。顯然地,核心對(duì)象使用完畢及時(shí)清除,這有利于系統(tǒng)效率的提高,所以程序員還是養(yǎng)成及時(shí)CoseHandle()這一習(xí)慣為好。
需要注意的是:你不可以依賴“因線程結(jié)束而清理所有被這一線程產(chǎn)生的核心對(duì)象”。許多核心對(duì)象,是被進(jìn)程所擁有,而非線程所擁有,在進(jìn)程結(jié)束之前不能夠清理它們。
為什么可以在不結(jié)束線程的情況下關(guān)閉其句柄?
線程句柄是指向“線程核心對(duì)象”,而不是線程本身。
當(dāng)你調(diào)用CloseHandle()時(shí),只不過表示希望自己和此核心對(duì)象不再有任何瓜葛。CloseHandle()唯一做的事情就是把引用計(jì)數(shù)減1。如果該值為0,對(duì)象就會(huì)自動(dòng)地被操作系統(tǒng)銷毀。
“線程核心對(duì)象”引用到的那個(gè)線程也會(huì)令核心對(duì)象開啟。因此,線程的默認(rèn)引用計(jì)數(shù)為2。當(dāng)你調(diào)用CloseHandle()時(shí),引用計(jì)數(shù)減1,當(dāng)線程結(jié)束時(shí),引用計(jì)數(shù)再減1。只有兩個(gè)事情都發(fā)生了(次序不限),這個(gè)對(duì)象才會(huì)被真正地銷毀。
線程結(jié)束代碼(Exit Code)
BOOL GetExitCodeThread(
HANDLE hThread, // handle to the thread
LPDWORD lpExitCode // address to receive termination status
);
如果成功,GetExitCodeThread()返回TRUE,否則FALSE。如果失敗,可以調(diào)用GetLastError()找出原因。如果線程已經(jīng)結(jié)束,那么線程的結(jié)束碼會(huì)被存放在lpExitCode參數(shù)中帶回來。如果線程尚未結(jié)束,lpExitCode帶回的是STILL_ACTIVE。
如果線程已經(jīng)結(jié)束,lpExitCode參數(shù)返回值可能是:
1. ExitThread()或TerminateThread()函數(shù)中定義的值;
2. 線程函數(shù)的返回值;
3. 擁有線程的進(jìn)程的退出值(進(jìn)程終止會(huì)強(qiáng)制線程終止)。
需要注意的是:不可根據(jù)GetExitCodeThread()返回值判斷線程是否還在運(yùn)行。如果線程還在運(yùn)行,尚未有所謂的結(jié)束碼時(shí),也會(huì)傳回TRUE(此時(shí)lpExitCode返回STILL_ACTIVE)。
結(jié)束一個(gè)線程
1) 線程函數(shù)結(jié)束,結(jié)束線程;
2) 使用ExitThread();
3) 主線程結(jié)束了
VOID ExitThread(
DWORD dwExitCode // exit code for this thread
);
ExitThread()類似于C runtime library中的Exit()函數(shù)。放在該函數(shù)后的任何代碼,肯定不會(huì)被執(zhí)行。
結(jié)束主線程
程序啟動(dòng)后就執(zhí)行的那個(gè)線程被稱為主線程。主線程有兩個(gè)特點(diǎn):(1)它必須負(fù)責(zé)GUI(Graphic User Interface)程序中的主消息循環(huán);(2)這一線程的結(jié)束會(huì)使程序中的所有線程都被強(qiáng)迫結(jié)束,程序因此而結(jié)束。
需要注意的是,一個(gè)線程被強(qiáng)行終止可能會(huì)導(dǎo)致它沒有機(jī)會(huì)做清理工作。
所以,程序員的一個(gè)良好的習(xí)慣是:主線程結(jié)束前,應(yīng)優(yōu)雅等待其它所有線程的結(jié)束。
GDI線程和Worker線程
GDI線程的定義是:擁有消息隊(duì)列的線程。任何一個(gè)窗口的消息總是被產(chǎn)生這一窗口的線程抓住并處理。所有對(duì)此窗口的改變也都應(yīng)該由該線程完成。
一般而言,GUI線程絕不會(huì)去做那些不能夠馬上完成的事情。否則,界面就會(huì)“呆”住了。
Worker線程則只完成事務(wù)性的處理。也就是說,Worker線程不能夠產(chǎn)生窗口、對(duì)話框、消息框、或任何其它與UI有關(guān)的東西。
如果Worker線程需要輸入輸出錯(cuò)誤消息,它應(yīng)該授權(quán)給UI線程來做(比如發(fā)送消息),并且把結(jié)果通知給Worker線程。
初學(xué)多線程編程的程序員最容易犯的一個(gè)錯(cuò)誤就是在Worker線程中直接調(diào)用GDI函數(shù)。比如通知更新對(duì)話框界面UpdateData(FALSE),請(qǐng)求在主窗口的狀態(tài)行顯示提示信息,如此等等。
切記:窗口的改變應(yīng)該由GDI該線程完成,Worker線程中不能直接更新UI。
在MFC內(nèi),有工作者線程和界面線程,其中界面線程中其實(shí)也就是比工作者線程多了一個(gè)消息循環(huán),可在界面線程內(nèi)的初始化實(shí)例函數(shù)中創(chuàng)建對(duì)話框,或者文檔視圖,這樣整個(gè)GDI界面就可由獨(dú)立的消息循環(huán)來處理了,在這種情況下每個(gè)線程可獨(dú)立的處理GDI。當(dāng)然對(duì)于同一個(gè)GDI對(duì)象的訪問最好不要使用SendMessage而應(yīng)該使用PostMessage,因?yàn)榈谝粋€(gè)同步,而第二個(gè)是異步的,使用PostMessage時(shí)要求其參數(shù)傳遞的對(duì)象為全局對(duì)象,或堆中的變量,不能使用局部變量。
|
經(jīng)驗(yàn)總結(jié)
1) 各線程的數(shù)據(jù)要分離開來,避免使用全局變量;
2) 不要在線程之間共享GDI對(duì)象;
3) 確定你知道你的線程狀態(tài),不要徑自結(jié)束程序而不等待它們的結(jié)束;
4) 讓主線程處理用戶界面。