線程同步是指併發線程高效、有序的訪問共享資源所採用的技術,所謂同步,是指某一時刻只有一個線程可以訪問資源,只有當資源所有者主自動放棄了代碼或資源的所有權時,其他線程纔可以使用這些資源。
線程同步可以分別使用C#中的lock關鍵字、Monitor類、Interlocked類和Mutex類實現,下面對這4種實現方法進行詳細介紹。
1.使用C#中的lock關鍵字實現線程同步
lock關鍵字可以用來確保代碼塊完成運行,而不會被其他線程中斷,它是通過在代碼塊運行期間爲給定對象獲取互斥鎖來實現的。
lock語句以關鍵字lock開頭,它有一個作爲參數的對象,在該參數的後面還有一個一次只能由一個線程執行的代碼塊。Lock語句語法格式如下。
Object thisLock = new Object();
lock (thisLock)
{
//要運行的代碼塊
}
提供給lock語句的參數必須爲基於引用類型的對象,該對象用來定義鎖的範圍。嚴格來說,提供給lock語句的參數只是用來唯一標識由多個線程共享的資源,所以它可以是任意類實例,然而,實際上,此參數通常表示需要進行線程同步的資源。例如,如果一個容器對象將被多個線程使用,則可以將該容器傳遞給lock語句,而lock語句中的代碼塊將訪問該容器。只要其他線程在訪問該容器前先鎖定該容器,則對該對象的訪問將是安全同步的。
通常,最好避免鎖定public類型或不受應用程序控制的對象實例。例如,如果該實例可以被公開訪問,則lock(this)可能會有問題,因爲不受控制的代碼也可能會鎖定該對象,這將可能導致死鎖,即兩個或更多個線程等待釋放同一對象。出於同樣的原因,鎖定公共數據類型(相比於對象)也可能導致問題,鎖定字符串尤其危險,因爲字符串被公共語言運行庫(CLR)“暫留”,這意味着整個程序中任何給定字符串都只有一個實例,因此,只要在應用程序進程中的任何具有相同內容的字符串上放置了鎖,就將鎖定應用程序中該字符串的所有實例,因此,最好鎖定不會被暫留的私有或受保護成員。
說明:事實上lock語句是用Monitor類來實現的,它等效於try/finally語句塊,使用lock關鍵字通常比直接使用Monitor類更可取,一方面是因爲lock更簡潔,另一方面是因爲lock確保了即使受保護的代碼引發異常,也可以釋放基礎監視器。這是通過finally關鍵字來實現的,無論是否引發異常它都執行關聯的代碼塊。
例如創建一個控制檯應用程序,其中自定義了一個LockThread方法,該方法中使用lock關鍵字鎖定當前線程,然後在Main方法中通過Program的類對象調用LockThread自定義方法。代碼如下。
static void Main(string[] args)
{
Program myProgram = new Program();//實例化類對象
myProgram.LockThread();//調用鎖定線程方法
}
void LockThread()
{
lock (this) //鎖定當前線程,以實現同步
{
Console.WriteLine("鎖定線程以實現線程同步");
}
}
2.使用Monitor類實現線程同步
Monitor類提供了同步對對象的訪問機制,它通過向單個線程授予對象鎖來控制對對象的訪問,對象鎖提供限制訪問代碼塊(通常稱爲臨界區)的能力。當一個線程擁有對象鎖時,其他任何線程都不能獲取該鎖。
Monitor類的主要功能如下。
它根據需要與某個對象相關聯。
它是未綁定的,也就是說可以直接從任何上下文調用它。
不能創建Monitor類的實例。
Monitor類的常用方法及說明如表1所示。
表1 Monitor類的常用方法及說明
方法 |
說明 |
Enter |
在指定對象上獲取排他鎖 |
Exit |
釋放指定對象上的排他鎖 |
Pulse |
通知等待隊列中的線程鎖定對象狀態的更改 |
PulseAll |
通知所有的等待線程對象狀態的更改 |
TryEnter |
試圖獲取指定對象的排他鎖 |
Wait |
釋放對象上的鎖並阻止當前線程,直到它重新獲取該鎖 |
注意:使用Monitor類鎖定的是對象(即引用類型)而不是值類型。
例 創建一個控制檯應用程序,其中自定義了一個LockThread方法,該方法中首先使用Monitor類的Enter方法鎖定當前線程,然後再調用Monitor類的Exit方法釋放當前線程,最後在Main方法中通過Program的類對象調用LockThread自定義方法。代碼如下。
static void Main(string[] args)
{
Program myProgram = new Program();//實例化類對象
myProgram.LockThread();//調用鎖定線程方法
}
void LockThread()
{
Monitor.Enter(this); //鎖定當前線程
Console.WriteLine("鎖定線程以實現線程同步");
Monitor.Exit(this); //釋放當前線程
}
3.使用Mutex類實現線程同步
當兩個或更多線程需要同時訪問一個共享資源時,系統需要使用同步機制來確保一次只有一個線程使用該資源。Mutex類是同步基元,它只向一個線程授予對共享資源的獨佔訪問權。如果一個線程獲取了互斥體,則要獲取該互斥體的第二個線程將被掛起,直到第一個線程釋放該互斥體。Mutex類與監視器類似,它防止多個線程在某一時間同時執行某個代碼塊,然而與監視器不同的是,Mutex類可以用來使跨進程的線程同步。
可以使用WaitHandle.WaitOne方法請求互斥體的所屬權,擁有互斥體的線程可以在對WaitOne方法的重複調用中請求相同的互斥體而不會阻止其執行,但線程必須調用同樣多次數的ReleaseMutex方法以釋放互斥體的所屬權。Mutex類強制線程標識,因此互斥體只能由獲得它的線程釋放。
當用於進程間同步時,Mutex稱爲“命名Mutex”,因爲它將用於另一個應用程序,因此它不能通過全局變量或靜態變量共享。必須給它指定一個名稱,才能使兩個應用程序訪問同一個Mutex對象。
Mutex類的常用方法及說明如表1所示。
表1 Mutex類的常用方法及說明
方法 |
說明 |
Close |
在派生類中被重寫時,釋放由當前WaitHandle持有的所有資源 |
OpenExisting |
打開現有的已命名互斥體 |
ReleaseMutex |
釋放Mutex一次 |
SignalAndWait |
原子操作的形式,向一個WaitHandle發出信號並等待另一個 |
WaitAll |
等待指定數組中的所有元素都收到信號 |
WaitAny |
等待指定數組中的任一元素收到信號 |
WaitOne |
當在派生類中重寫時,阻止當前線程,直到當前的WaitHandle收到信號 |
使用Mutex類實現線程同步很簡單,首先實例化一個Mutex類對象,它的構造函數中比較常用的有public Mutex(bool initallyOwned),其中,參數initallyOwned指定了創建該對象的線程是否希望立即獲得其所有權,當在一個資源得到保護的類中創建Mutex類對象時,常將該參數設置爲false;然後在需要單線程訪問的地方調用其等待方法,等待方法請求Mutex對象的所有權,這時,如果該所有權被另一個線程所擁有,則阻塞請求線程,並將其放入等待隊列中,請求線程將保持阻塞,直到Mutex對象收到了其所有者線程發出將其釋放的信號爲止。所有者線程在終止時釋放Mutex對象,或者調用ReleaseMutex方法來釋放Mutex對象。
說明:儘管Mutex類可以用於進程內的線程同步,但是使用Monitor類通常更爲可取,因爲Monitor監視器是專門爲.NET Framework而設計的,因而它可以更好地利用資源。相比之下,Mutex類是Win32構造的包裝。儘管Mutex類比監視器更爲強大,但是相對於Monitor類,它所需要的互操作轉換更消耗計算資源。
例如創建一個控制檯應用程序,其中自定義了一個LockThread方法,該方法中首先使用Mutex類對象的WaitOne方法阻止當前線程,然後再調用Mutex類對象的ReleaseMutex方法釋放Mutex對象,即釋放當前線程,最後在Main方法中通過Program的類對象調用LockThread自定義方法。代碼如下。
static void Main(string[] args)
{
Program myProgram = new Program();//實例化類對象
myProgram.LockThread();//調用鎖定線程方法
}
void LockThread()
{
Mutex myMutex=new Mutex(false); //實例化Mutex類對象
myMutex.WaitOne();//阻止當前線程
Console.WriteLine("鎖定線程以實現線程同步");
myMutex.ReleaseMutex();//釋放Mutex對象
}