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