多線程程序設計(四)

3.2.5 互斥內核對象

互斥(Mutex)是一種用途非常廣泛的內核對象。能夠保證多個線程對同一共享資源的互斥訪問。同臨界區有些類似,只有擁有互斥對象的線程才具有訪問資源的權限,由於互斥對象只有一個,因此就決定了任何情況下此共享資源都不會同時被多個線程所訪問。當前佔據資源的線程在任務處理完後應將擁有的互斥對象交出,以便其他線程在獲得後得以訪問資源。與其他幾種內核對象不同,互斥對象在操作系統中擁有特殊代碼,並由操作系統來管理,操作系統甚至還允許其進行一些其他內核對象所不能進行的非常規操作。爲便於理解,可參照圖3.8給出的互斥內核對象的工作模型:

圖3.8 使用互斥內核對象對共享資源的保護

圖(a)中的箭頭爲要訪問資源(矩形框)的線程,但只有第二個線程擁有互斥對象(黑點)並得以進入到共享資源,而其他線程則會被排斥在外(如圖(b)所示)。當此線程處理完共享資源並準備離開此區域時將把其所擁有的互斥對象交出(如圖(c)所示),其他任何一個試圖訪問此資源的線程都有機會得到此互斥對象。

以互斥內核對象來保持線程同步可能用到的函數主要有CreateMutex、OpenMutex、ReleaseMutex、WaitForSingleObject和WaitForMultipleObjects等。在使用互斥對象前,首先要通過CreateMutex或OpenMutex創建或打開一個互斥對象。CreateMutex函數原型如下:

HANDLE CreateMutex(

 LPSECURITY_ATTRIBUTES lpMutexAttributes,     // 安全屬性指針

 BOOL bInitialOwner,                                            // 初始擁有者

 LPCTSTR lpName                                               // 互斥對象名

);

參數bInitialOwner主要用來控制互斥對象的初始狀態。一般多將其設置爲FALSE,以表明互斥對象在創建時並沒有爲任何線程所佔有。如果在創建互斥對象時指定了對象名,那麼可以在本進程其他地方或是在其他進程通過OpenMutex函數得到此互斥對象的句柄。OpenMutex函數原型爲:

HANDLE OpenMutex(
 DWORD dwDesiredAccess, // 訪問標誌
 BOOL bInheritHandle, // 繼承標誌
 LPCTSTR lpName // 互斥對象名
);

當目前對資源具有訪問權的線程不再需要訪問此資源而要離開時,必須通過ReleaseMutex函數來釋放其擁有的互斥對象,其函數原型爲:

BOOL ReleaseMutex(HANDLE hMutex);

其惟一的參數hMutex爲待釋放的互斥對象句柄。至於WaitForSingleObject和WaitForMultipleObjects等待函數在互斥對象保持線程同步中所起的作用與在其他內核對象中的作用是基本一致的,也是等待互斥內核對象的通知。但是這裏需要特別指出的是:在互斥對象通知引起調用等待函數返回時,等待函數的返回值不再是通常的WAIT_OBJECT_0(對於WaitForSingleObject函數)或是在WAIT_OBJECT_0到WAIT_OBJECT_0+nCount-1之間的一個值(對於WaitForMultipleObjects函數),而是將返回一個WAIT_ABANDONED_0(對於WaitForSingleObject函數)或是在WAIT_ABANDONED_0到WAIT_ABANDONED_0+nCount-1之間的一個值(對於WaitForMultipleObjects函數),以此來表明線程正在等待的互斥對象由另外一個線程所擁有,而此線程卻在使用完共享資源前就已經終止。除此之外,使用互斥對象的方法在等待線程的可調度性上同使用其他幾種內核對象的方法也有所不同,其他內核對象在沒有得到通知時,受調用等待函數的作用,線程將會掛起,同時失去可調度性,而使用互斥的方法卻可以在等待的同時仍具有可調度性,這也正是互斥對象所能完成的非常規操作之一。
  在編寫程序時,互斥對象多用在對那些爲多個線程所訪問的內存塊的保護上,可以確保任何線程在處理此內存塊時都對其擁有可靠的獨佔訪問權。下面給出的示例代碼即通過互斥內核對象hMutex對共享內存快g_cArray[]進行線程的獨佔訪問保護。下面是示例代碼:

// 互斥對象

HANDLE hMutex = NULL;

char g_cArray[10];

UINT ThreadProc1(LPVOID pParam)

{

 // 等待互斥對象通知

 WaitForSingleObject(hMutex, INFINITE);

 // 對共享資源進行寫入操作

 for (int i = 0; i < 10; i++)

 {

  g_cArray[i] = 'a';

  Sleep(1);

 }

 // 釋放互斥對象

 ReleaseMutex(hMutex);

 return 0;

}

UINT ThreadProc2(LPVOID pParam)

{

 // 等待互斥對象通知

 WaitForSingleObject(hMutex, INFINITE);

 // 對共享資源進行寫入操作

 for (int i = 0; i < 10; i++)

 {

  g_cArray[10 - i - 1] = 'b';

  Sleep(1);

 }

 // 釋放互斥對象

 ReleaseMutex(hMutex);

 return 0;

}

線程的使用使程序處理能夠更加靈活,而這種靈活同樣也會帶來各種不確定性的可能。尤其是在多個線程對同一公共變量進行訪問時。雖然未使用線程同步的程序代碼在邏輯上或許沒有什麼問題,但爲了確保程序的正確、可靠運行,必須在適當的場合採取線程同步措施。

3.2.6 線程局部存儲

線程局部存儲(thread-local storage, TLS)是一個使用很方便的存儲線程局部數據的系統。利用TLS機制可以爲進程中所有的線程關聯若干個數據,各個線程通過由TLS分配的全局索引來訪問與自己關聯的數據。這樣,每個線程都可以有線程局部的靜態存儲數據。

用於管理TLS的數據結構是很簡單的,Windows僅爲系統中的每一個進程維護一個位數組,再爲該進程中的每一個線程申請一個同樣長度的數組空間,如圖3.9所示。

圖3.9 TSL機制在內部使用的數據結構

運行在系統中的每一個進程都有圖3.9所示的一個位數組。位數組的成員是一個標誌,每個標誌的值被設爲FREE或INUSE,指示了此標誌對應的數組索引是否在使用中。Windodws保證至少有TLS_MINIMUM_AVAILABLE(定義在WinNT.h文件中)個標誌位可用。

動態使用TLS的典型步驟如下。

(1)主線程調用TlsAlloc函數爲線程局部存儲分配索引,函數原型爲:

DWORD TlsAlloc(void); // 返回一個TLS索引

如上所述,系統爲每一個進程都維護着一個長度爲TLS_MINIMUM_AVAILABLE的位數組,TlsAlloc的返回值就是數組的一個下標(索引)。這個位數組的惟一用途就是記憶哪一個下標在使用中。初始狀態下,此位數組成員的值都是FREE,表示未被使用。當調用TlsAlloc的時候,系統會挨個檢查這個數組中成員的值,直到找到一個值爲FREE的成員。把找到的成員的值由FREE改爲INUSE後,TlsAlloc函數返回該成員的索引。如果不能找到一個值爲FREE的成員,TlsAlloc函數就返回TLS_OUT_OF_INDEXES(在WinBase.h文件中定義爲-1),意味着失敗。

例如,在第一次調用TlsAlloc的時候,系統發現位數組中第一個成員的值是FREE,它就將此成員的值改爲INUSE,然後返回0。

當一個線程被創建時,Windows就會在進程地址空間中爲該線程分配一個長度爲TLS_MINIMUM_AVAILABLE的數組,數組成員的值都被初始化爲0。在內部,系統將此數組與該線程關聯起來,保證只能在該線程中訪問此數組中的數據。如圖3.7所示,每個線程都有它自己的數組,數組成員可以存儲任何數據。

(2)每個線程調用TlsSetValue和TlsGetValue設置或讀取線程數組中的值,函數原型爲:

BOOL TlsSetValue(

DWORD dwTlsIndex,     // TLS 索引

LPVOID lpTlsValue                   // 要設置的值

);

LPVOID TlsGetValue(DWORD dwTlsIndex );       // TLS索引

TlsSetValue函數將參數lpTlsValue指定的值放入索引爲dwTlsIndex的線程數組成員中。這樣,lpTlsValue的值就與調用TlsSetValue函數的線程關聯了起來。此函數調用成功,會返回TRUE。

調用TlsSetValue函數,一個線程只能改變自己線程數組中成員的值,而沒有辦法爲另一個線程設置TLS值。到現在爲止,將數據從一個線程傳到另一個線程的惟一方法是在創建線程時使用線程函數的參數。

TlsGetValue函數的作用是取得線程數組中索引爲dwTlsIndex的成員的值。

TlsSetValue和TlsGetValue分別用於設置和取得線程數組中的特定成員的值,而它們使用的索引就是TlsAlloc函數的返回值。這就充分說明了進程中惟一的位數組和各線程數組的關係。例如,TlsAlloc返回3,那就說明索引3被此進程中的每一個正在運行的和以後要被創建的線程保存起來,用以訪問各自線程數組中對應的成員的值。

(3)主線程調用TlsFree釋放局部存儲索引。函數的惟一參數是TlsAlloc返回的索引。

利用TLS可以給特定的線程關聯一個數據。比如下面的例子將每個線程的創建時間與該線程關聯了起來,這樣,在線程終止的時候就可以得到線程的生命週期。整個跟蹤線程運行時間的例子的代碼如下:

#include <stdio.h>                                   // 03UseTLS工程下

#include <windows.h>            

#include <process.h>

// 利用TLS跟蹤線程的運行時間

DWORD g_tlsUsedTime;

void InitStartTime();

DWORD GetUsedTime();

UINT __stdcall ThreadFunc(LPVOID)

{       int i;

         // 初始化開始時間

         InitStartTime();

         // 模擬長時間工作

         i = 10000*10000;

         while(i--){}

         // 打印出本線程運行的時間

         printf(" This thread is coming to end. Thread ID: %-5d, Used Time: %d \n",

                                                                                                       ::GetCurrentThreadId(), GetUsedTime());

         return 0;

}

int main(int argc, char* argv[])

{       UINT uId;

         int i;

         HANDLE h[10];

         // 通過在進程位數組中申請一個索引,初始化線程運行時間記錄系統

         g_tlsUsedTime = ::TlsAlloc();

         // 令十個線程同時運行,並等待它們各自的輸出結果

         for(i=0; i<10; i++)

         {       h[i] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);         }

         for(i=0; i<10; i++)

         {       ::WaitForSingleObject(h[i], INFINITE);

                   ::CloseHandle(h[i]);      }

         // 通過釋放線程局部存儲索引,釋放時間記錄系統佔用的資源

         ::TlsFree(g_tlsUsedTime);

         return 0;

}

// 初始化線程的開始時間

void InitStartTime()

{       // 獲得當前時間,將線程的創建時間與線程對象相關聯

         DWORD dwStart = ::GetTickCount();

         ::TlsSetValue(g_tlsUsedTime, (LPVOID)dwStart);

}

// 取得一個線程已經運行的時間

DWORD GetUsedTime()

{       // 獲得當前時間,返回當前時間和線程創建時間的差值

         DWORD dwElapsed = ::GetTickCount();

         dwElapsed = dwElapsed - (DWORD)::TlsGetValue(g_tlsUsedTime);

         return dwElapsed;

}

GetTickCount函數可以取得Windows從啓動開始經過的時間,其返回值是以毫秒爲單位的已啓動的時間。

一般情況下,爲各線程分配TLS索引的工作要在主線程中完成,而分配的索引值應該保存在全局變量中,以方便各線程訪問。上面的例子代碼很清除地說明了這一點。主線程一開始就使用TlsAlloc爲時間跟蹤系統申請了一個索引,保存在全局變量g_tlsUsedTime中。之後,爲了示例TLS機制的特點同時創建了10個線程。這10個線程最後都打印出了自己的生命週期,如圖3.10所示。

3.10 各線程的生命週期

這個簡單的線程運行時間記錄系統僅提供InitStartTime和GetUsedTime兩個函數供用戶使用。應該在線程一開始就調用InitStartTime函數,此函數得到當前時間後,調用TlsSetValue將線程的創建時間保存在以g_tlsUsedTime爲索引的線程數組中。當想查看線程的運行時間時,直接調用GetUsedTime函數就行了。這個函數使用TlsGetValue取得線程的創建時間,然後返回當前時間和創建時間的差值。

另外用於線程同步的內核對象還有互斥體和信號量,它們的用法也比較簡單,這裏就不介紹了

發佈了24 篇原創文章 · 獲贊 18 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章