621
京東網上商城
核心編程6——線程
Note 0:
了進程實際是由兩個組件組成的:一個進程內核對象和一個地址空間.類似地,線程也由兩個組件組成:
一個是線程的內核對象,操作係統用它管理線程.內核對象還是係統用來存放線程統計信息的地方.
一個線程堆棧,用於維護線程執行時所需的所有函數參數和局部變量.
Note 1:
進程是有惰性的.進程從來不執行任何東西,它隻是一個線程的容器.線程必然是在某個進程的上下文中創建的,而且會在這個進程內部"終其一生".這意味著線程要在其進程的地址空間內執行代碼和處理數據.此外,這些線程還共享內核對象句柄,因為句柄表是針對每一個進程的,而不是針對每一個線程.相較於線程,進程所使用的係統資源更多.其原因在於地址空間.為一個進程創建一個虛擬的地址空間需要大量係統資源.係統中會發生大量的記錄活動,而這需要用到大量內存.而且,由於.exe和.dll文件要加載到一個地址空間,所以還需要用到文件資源.另一方麵,線程使用的係統資源要少得多.事實上,線程隻有一個內核對象和一個堆棧;幾乎不涉及記錄活動,所以不需要占用多少內存.
Note 2:
線程描述了進程內部的一個執行路徑.每次初始化進程時,係統都會創建一個主線程.對於用Microsoft C/C++編譯器生成的應用程序,這個線程首先會執行C/C++運行庫的啟動代碼,後者調用入口函數(_tmain或_tWinMain),並繼續執行,直至入口函數返回C/C++運行庫的啟動代碼,後者最終將調用ExitProcess.對於許多應用程序來說,這個主線程是應用程序惟一需要的線程.但是,進程也可以創建額外的線程來幫助它們完成自己的工作.
Note 3:
操作係統的Windows Indexing Services(Windows索引服務)創建了一個低優先級的線程.此線程定期醒來.並對硬盤上的特定區域的文件內容進行索引.Windows索引服務極大改進了性能.因為一旦成功建立索引.就不必在每次搜索時都打開、掃描和關閉硬盤上的每一個文件.配合這種索引服務.Microsoft Windows Vista提供了一套高級的搜索功能.
Note 4:
如果想創建一個或
多個輔助線程.隻需讓一個正在運行的線程調用CreateThread:
HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa,
DWORD cbStackSize,
PTHREAD_START_ROUTINE pfnStartAddr,
PVOID pvParam,
DWORD dwCreateFlags,
PDWORD pdwThreadID);
調用CreateThread時.係統會創建一個線程內核對象.這個線程內核對象不是線程本身.而是一個較小的數據結構.操作係統用這個結構來管理線程.可以把線程內核對象想象為一個由線程統計信息構成的小型數據結構.這與進程和進程內核對象之間的關係是相同的
係統將進程地址空間的內存分配給線程堆棧使用.新線程在與負責創建的那個線程相同的進程上下文中運行.因此.新線程可以訪問進程內核對象的所有句柄、進程中的所有內存以及同一個進程中其他所有線程的堆棧.這樣一來.同一個進程中的多個進程可以很容易地互相通信.
Note 5:
CreateThread函數是用於創建線程的Windows函數.不過.如果寫的是C/C++代碼.就絕對不要調用CreateThread.相反.正確的選擇是使用Microsoft C++運行庫函數_beginthreadex.如果使用的不是Microsoft C++編譯器.你的編譯器的提供商應該提供類似的函數來替代CreateThread.不管這個替代函數是什麼.都必須使用它.
Note 6:
使用鏈接器的/STACK開關來控製線程堆棧使用多少地址空間.如下所示:
/STACK:[reserve] [,commit]
reserve參數用於設置係統將為線程堆棧預留多少地址空間.默認是1 MB(在Itanium芯片組上.默認大小為 4 MB).commit參數指定最初應為堆棧的保留區域提交多少物理存儲空間.默認是1頁.隨著線程中的代碼開始執行.需要的存儲空間可能不止1頁.如果線程溢出它的堆棧.會產生異常.(有關線程堆棧和堆棧溢出異常的詳情.請參見第16章.有關常見異常處理的詳情.請參見第23章.)係統將捕獲這種異常.並將另一個頁(或者為commit參數指定的任何大小)提交給保留空間.這樣一來.線程堆棧就可以根據需要動態地增大.
Note 7:
保留空間的容量設定了堆棧空間的上限.這樣才能捕獲代碼中的無窮遞歸bug.例如.假設你寫了一個函數以遞歸方式調用其自身.而且這個函數存在一個bug.會導致無窮遞歸.每次此函數調用自身時.都會在內存堆棧上創建一個新的堆棧幀.如果係統沒有設定堆棧空間的上限.這個遞歸調用的函數就永遠不會終止調用自身.進程的所有地址空間都會被分配出去.大量物理存儲會提交給堆棧.通過設置堆棧空間的上限.可以防止應用程序耗盡物理內存區域.而且還可以盡早察覺程序中的bug.
Note 8:
創建多個線程時.可以讓它們使用同一個函數地址作為起點.這樣做完全合法.而且非常有用.(可以傳遞了不同的pvParam值,來區分不同的線程所調用相同的函數.)
Note 9:
TerminateThread函數是異步的.也就是說.它告訴係統你想終止線程.但在函數返回時.並不保證線程已經終止了.如果需要確定線程已終止運行.還需要調用WaitForSingleObject或類似的函數.並向其傳遞線程的句柄.
Note 10:
如果通過返回或調用ExitThread函數的方式來終止一個線程的運行.該線程的堆棧也會被銷毀.但是.如果使用的是TerminateThread.那麼除非擁有此線程的進程終止運行.否則係統不會銷毀這個線程的堆棧.Microsoft故意以這種方式來實現TerminateThread.否則.假如其他還在運行的線程要引用被"殺死"的那個線程的堆棧上的值.就會引起訪問衝突.讓被"殺死"的線程的堆棧保留在內存中.其他的線程就可以繼續正常運行.
此外.動態鏈接庫(DLL)通常會在線程終止運行時收到通知.不過.如果線程是用TerminateThread強行"殺死"的.則DLL不會收到這個通知.其結果是不能執行正常的清理工作.
Note 11:
線程終止運行時.會發生下麵這些事情:
線程擁有的所有用戶對象句柄會被釋放.在Windows中.大多數對象都是由包含了"創建這些對象的線程"的進程擁有的.但是.一個線程有兩個User對象:窗口(window)和掛鉤(hook).一個線程終止運行時.係統會自動銷毀由線程創建或安裝的任何窗口.並卸載由線程創建或安裝的任何掛鉤.其他對象隻有在擁有線程的進程終止時才被銷毀.
線程的退出代碼從STILL_ACTIVE變成傳給ExitThread或TerminateThread的代碼.
線程內核對象的狀態變為signaled.
如果線程是進程中的最後一個活動線程.係統認為進程也終止了.
線程內核對象的使用計數遞減1.
Note 19:
對CreateThread函數的一個調用導致係統創建一個線程內核對象.該對象最初的使用計數為2.(除非線程終止,而且從CreateThread返回的句柄關閉,否則線程內核對象不會被銷毀.)該線程內核對象的其他屬性也被初始化:暫停計數被設為1,退出代碼被設為STILL_ACTIVE (0x103),而且對象被設為nonsignaled狀態.
Note 20:
一旦創建了內核對象,係統就分配內存,供線程的堆棧使用.此內存是從進程的地址空間內分配的,因為線程沒有自己的地址空間.然後,係統將兩個值寫入新線程堆棧的最上端.(線程堆棧始終是從高位內存地址向低位內存地址構建的.)寫入線程堆棧的第一個值是傳給CreateThread函數的pvParam參數的值.緊接在它下方的是傳給CreateThread函數的pfnStartAddr值.
Note 21:
每個線程都有其自己的一組CPU寄存器,稱為線程的上下文(context).上下文反映了當線程上一次執行時,線程的CPU寄存器的狀態.線程的CPU寄存器全部保存在一個CONTEXT結構(在WinNT.h頭文件中定義).CONTEXT結構本身保存在線程內核對象中.
Note 22:
當線程的內核對象被初始化的時候,CONTEXT結構的堆棧指針寄存器被設為pfnStartAddr在線程堆棧中的地址.而指令指針寄存器被設為RtlUserThreadStart函數(該函數未見於正式文檔)的地址,此函數是NTDLL.dll模塊導出的.
RtlUserThreadStart函數的基本用法如下:
VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) {
__try {
ExitThread((pfnStartAddr)(pvParam));
}
__except(UnhandledExceptionFilter(GetExceptionInformation())) {
ExitProcess(GetExceptionCode());
}
// NOTE: We never get here.
}
Note 23:
線程完全初始化好之後,係統將檢查CREATE_SUSPENDED標誌是否傳給CreateThread函數.如果此標記沒有傳遞,係統將線程的暫停計數遞增至0;隨後,線程就可以調度給一個處理器去執行.然後,係統在實際的CPU寄存器中加載上一次在線程上下文中保存的值.現在,線程可以在其進程的地址空間中執行代碼並處理數據了.
Note 24:
新線程執行RtlUserThreadStart函數的時候,將發生以下事情:
圍繞你的線程函數,會設置一個結構化異常處理(Structured Exception Handling,SEH)幀.這樣一來,線程執行期間所產生的任何異常都能得到係統的默認處理.
係統調用你的線程函數,把你傳給CreateThread函數的pvParam參數傳給它.
線程函數返回時,RtlUserThreadStart調用ExitThread,將你的線程函數的返回值傳給它.線程內核對象的使用計數遞減,而後線程停止執行.
如果線程產生了一個未被處理的異常,RtlUserThreadStart函數所設置的SEH幀會處理這個異常.通常,這意味著會向用戶顯示一個消息框,而且當用戶關閉此消息框時,RtlUserThreadStart會調用ExitProcess來終止整個進程,而不隻是終止有問題的線程.
Note 25:
當RtlUserThreadStart開始執行時,它會調用C/C++運行庫的啟動代碼,後者初始化繼而調用你的_tmain或_tWinMain函數.你的入口函數返回時,C/C++運行時啟動代碼會調用ExitProcess.所以對於C/C++應用程序來說,主線程永遠不會返回到RtlUserThreadStart函數.
Note 26:
Visual Studio附帶了4個原生的C/C++運行庫,還有2個庫麵向Microsoft.NET的托管環境.注意,所有這些庫都支持多線程開發:不再有單獨的一個C/C++庫專門針對單線程開發.下麵對這些庫進行了描述.
Microsoft Visual Studio附帶的C/C++庫
庫名稱 描述
LibCMt.lib 庫的靜態鏈接Release版本
LibCMtD.lib 庫的靜態鏈接Debug版本
MSVCRt.lib 導入庫,用於動態鏈接MSVCR80.dll 庫的Release版本. (這是新建項目時的默認庫)
MSVCRtD.lib 導入庫,用於動態鏈接MSVCR80D.dll庫的Debug版本
MSVCMRt.lib 導入庫,用於托管/原生代碼混合
MSVCURt.lib 導入庫,編譯成百分之百純MSIL代碼
-------------------------------------------------------------------
最後更新:2017-04-03 15:22:09