1. 程式人生 > >Windows核心程式設計之執行緒

Windows核心程式設計之執行緒

執行緒組成兩部分:

1. 一個執行緒的核心物件,作業系統用它管理執行緒。

2. 一個執行緒棧,用於維護執行緒執行時所需的所有函式引數和區域性變數。

何時建立執行緒?舉例:

作業系統的Windows Indexing Services,磁碟碎片整理程式等,都是使用多執行緒進行效能優化的例子。


執行緒內幕:

 執行緒的上下文context物件 ,用於執行緒的排程,如下這張圖詳細的介紹,執行緒核心物件以及執行緒的地址空間,從而知道執行緒執行原理

              執行緒核心物件                                                                                             執行緒棧


執行緒建立時,會先建立一個執行緒核心物件(分配在程序的地址空間上),如上圖,儲存上下文context(一個資料結構)及一些統計資訊,具體包括:

1.暫存器SP:指向棧中執行緒函式指標的地址

2.暫存器IP:指向裝載的NTDLL.dll裡RtlUserThreadStart函式地址

3.Usage Count:引用計數,初始化為2

4.Suspend Count:掛起數,初始化為1。

5.ExitCode:退出程式碼,執行緒在執行時為STILL_ACTIVE(且初始化為該值)

6.Signaled:初始化為未觸發狀態

首先看下執行緒的排程:

每個執行緒都有一個CONTEXT結構,儲存線上程核心物件中。大約每隔20ms windows就會檢視所有當前存在的執行緒核心物件。並在可排程的執行緒核心物件中選擇一個,將其儲存在CONTEXT結構的值載入cpu暫存器。這被稱為上下文切換。大約又過20ms  windows將當前cpu暫存器存回核心物件,執行緒被掛起。Windows再次檢查核心物件,並在可排程的核心物件中選擇一個進行排程。此過程不斷重複直到系統關閉。Windows被稱為搶佔式多執行緒系統,系統可以在任何時刻停止一個執行緒而另行排程另外一個執行緒。我們對此可以有一些控制,但是許可權很小。我們無法保證執行緒總在執行或者獲得整個處理器。當然Windows提供很多API函式用於執行緒的掛起和恢復,睡眠,切換等。當然為了能夠統計一個演算法的執行時間,可以開啟一個執行緒採用一些API函式精確統計去除執行緒切換而損失的時間。

其次分析一下CONTEXT結構以及如何獲取和設定上下文CONTEXT結構:

CONTEXT結構包括以下部分:

   CONTEXT_CONTROL:包含CPU的控制暫存器,比如指今指標,堆疊指標,標誌和函式返回地址..AX, BX, CX, DX, SI, D
     CONTEXT_INTEGER:用於標識CPU的整數暫存器.DS, ES, FS, GS
   CONTEXT_FLOATING_POINT:用於標識CPU的浮點暫存器.
     CONTEXT_SEGMENTS:用於標識CPU的段暫存器.SS:SP, CS:IP, FLAGS, BP
   CONTEXT_DEBUG_REGISTER:用於標識CPU的除錯暫存器.  
   CONTEXT_EXTENDED_REGISTERS:用於標識CPU的擴充套件暫存器I
   CONTEXT_FULL:相當於CONTEXT_CONTROL or CONTEXT_INTEGER or   CONTEXT_SEGMENTS,即這三個標誌的組合

我們可以使用GetThreadContext函式來檢視執行緒核心物件的內部,並獲取當前CPU暫存器狀態的集合。

BOOL GetThreadContext (

HANDLE  hThread,

PCONTEXT  pContext)

若要呼叫該函式,只需指定一個CONTEXT結構,對某些標誌(該結構的ContextFlags成員)進行初始化,指明想要收回哪些暫存器,並將該結構的地址傳遞給GetThreadContext 。然後該函式將資料填入你要求的成員。

在呼叫GetThreadContext函式之前,應該呼叫SuspendThread,否則,執行緒可能剛好被排程,這樣一來,執行緒的上下文就和所獲取的資訊不一致了。

示例程式碼如下: 

         CONTEXT Context;                  //定義一個CONTEXT結構

         Context.ContextFlags = CONTEXT_CONTROL;    //告訴系統我們想獲取執行緒控制暫存器的內容   

         GetThreadContext(hThread, &Context);      //呼叫GetThreadContext獲取相關資訊

Ps:在呼叫GetThreadContext函式之前,必須首先初始化CONTEXT結構的ContextFlags成員。

要獲得執行緒的所有重要的暫存器(也就是微軟認為最常用的暫存器),應該像下面一樣初始化ContextFlags

Context.ContextFlags = CONTEXT_FULL;

WinNT. h標頭檔案中,定義了CONTEXT_FULLCONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS

當然,我們還可以通過呼叫SetThreadContext函式來改變結構中的成員,並把新的暫存器值放回執行緒的核心物件中

BOOL SetThreadContext (

HANDLE  hThread,

CONST CONTEXT  *pContext)

同樣,如果要改變哪個執行緒的上下文,應該先暫停該執行緒。       

         CONTEXT Context;      //定義一個CONTEXT結構      

         SuspendThread(hThread);  //掛起執行緒  

         Context.ContextFlags = CONTEXT_CONTROL;   //獲取當前上下文的值

         GetThreadContext(hThread, &Context);

         Context.Eip = 0x00010000;      //Eip欄位儲存的是指令指標,現在讓指令指標指向地址 0x00010000

         Context.ContextFlags = CONTEXT_CONTROL;

         SetThreadContext(hThread, &Context);   //重新設定執行緒上下文

         ResumeThread(hThread);         //恢復執行緒,現線上程開始從0x00010000這個地方開始執行指令

當然作業系統對執行緒的還有其他的控制,為了提升CPU效能:

1. 執行緒的優先順序劃分以及優先順序的提升方法

2. CPU關聯性,可以讓執行緒在指定的CPU上執行,以提升程式和CPU效能。


執行緒能夠共享程序的所有資料,比如全域性變數,靜態變數,全域性資料結構,核心物件等,但是多執行緒同時訪問會造成執行緒不安全,為此執行緒需要同步,下面介紹一下執行緒的同步方式:

使用者模式下的同步方式:

1.原子訪問:IterLocked系列函式。原理Interlocked函式會在總線上維持一個匯流排訊號。

其中包括InterlockedExchangeAdd或者InterlockedExchangeAdd64遞增第二引數值,InterlockedIncrement函式實現加1功能。

其餘還有InterlockedExchange或者InterlockedExchange64或者InterlockedExchangePointer等。原子訪問適合對整數或者bool型資料的操作。

2. 快取記憶體行

  晶片設計方式,可以避免過頻繁的訪問記憶體匯流排。為保證快取記憶體行的效能,設計資料結構時應該保證只讀資料和可讀寫資料分開。

3. Volatile變數,為了防止編譯器優化,保證程式每次都從變數的記憶體處去讀寫資料,如果傳入的是變數的地址就沒有必要使用該變量了。

4. 關鍵段:將共享資源以原子方式進行訪問,共享資源可以是多行程式碼。將關鍵段的全域性變數CRITICAL_SECTION g_cs;

  進入資源EnterCriticalSection,離開資源LeaveCriticalSection,缺點是無法再不同程序之間進行同步,容易造成死鎖,因為不能設定關鍵段指定一個最長的等待時間

5. Slim讀寫鎖,與關鍵段不同的是允許我們區分哪些想要讀取資源的值的執行緒和想要更新資源的值的執行緒。讓所有讀取者執行緒在同一時刻訪問共享資源,只有當寫入者執行緒想要對資源進行更新時才進行同步。

6. 條件變數:有些情況下,如果如讀寫鎖中的讀取這執行緒沒有資料可以讀取,那麼它應該將鎖釋放並等待,直到寫入者執行緒產生了新的資料。寫入者執行緒同樣。其中Windows通過SleepConditionVariableCS或者SleepConditonVariableSRW函式。如果條件滿足的時候會呼叫WakeConditionVariable或者WakeAllConditionVariable

核心模式下的同步方式:

1.等待函式 WaitForSingleObject(HANDLE hObject, DWORD dwilliseconds);等待函式意義是一個執行緒資源進入等待狀態,直到指定的核心物件被觸發為止。WaitForSingleObjects();

2. 事件核心物件:包含使用計數,標誌自動重置事件和手動重置事件的bool值,事件有沒有觸發的bool值。建立一個事件CreateEvent或者CreateEventEx 開啟一個事件OpenEvent. 設定事件的狀態SetEvent  設定事件為未觸發狀態ResetEvent。注意:手動重置事件和自動重置的時間不太相同。

3. 可等待的計時器核心物件,它會在某個指定的時間觸發或者每隔一段時間觸發一次。建立可等待計時器,CreateWaitableTimer函式。或者開啟已存在的可等待計時器OpenWaitableTimer

4. 訊號量物件:用來對資源進行計數。

5.互斥量物件:確保一個執行緒獨佔對一個資源的訪問。