排它鎖主要用來保證,在一段時間內,只有一個線程可以訪問某一段代碼。兩種主要類型的排它鎖是lock和Mutex。Lock和Mutex相比構造起來更方便,運行的也更快。但是Mutex可以在同一個機器上的不同進程使用。
C#中的lock關鍵字,實際上是Monitor.Enter,Monitor.Exist的一個簡寫。在.NET 1.0,2.0,3.0 版本的c#中,lock會被編譯成如下代碼:
Monitor.Enter(_locker); try { if (_val2 != 0) Console.WriteLine(_val1 / _val2); _val2 = 0; } finally { Monitor.Exit(_locker); }
如果你沒有調用Monitor.Enter而直接調用Monitor.Exit會引發異常。
想象一下上面這段代碼,如果再Monitor.Enter之後,try之前,線程出現了異常(比如被終止),在這種情況下,finally中的Exit方法就永遠不會被執行,也就導致了這個鎖不會被釋放。為了避免這種情況,CLR 4.0的設計者重載了Monitor.Enter方法:
public static void Enter (object obj, ref bool lockTaken);
如果當前線程由於某些異常導致鎖沒有被獲取到,lockTake值會為false,因此在CLR 4.0中,lock會被解釋成如下代碼:
bool lockTaken = false; try { Monitor.Enter(_locker, ref lockTaken); // Do your stuff... } finally { if (lockTaken) Monitor.Exit(_locker); }
Monitor也提供了了一個TryEnter方法,允許你設置一個超時時間,避免當前線程長時間獲取不到鎖而一直等待。
你需要選擇一個對所有線程都可見的對象進行lock(obj)來確保程序能夠按照你的意圖執行。如果比不了解C#語言中的某些特性,lock可能不會按照你 期望來執行。
一個基本的規則,你需要對任意的寫操作,或者可修改的字段進行lock。即使是一個賦值操作,或者累加操作,你也不能假設他是線程安全的。
例如下面代碼不是線程安全的:
class ThreadUnsafe { static int _x; static void Increment() { _x++; } static void Assign() { _x = 123; } }
你需要這樣寫:
class ThreadSafe { static readonly object _locker = new object(); static int _x; static void Increment() { lock (_locker) _x++; } static void Assign() { lock (_locker) _x = 123; } }
如果你看過一些BCL類庫裡面的實現,你可以能會發現,某些情況下會使用InterLocked類,而不是lock,我們會在後面介紹。
你在閱讀一些文檔的時候,有的文檔可能會說lock或者Monitor.Enter是reentrant(可重入的),那麼我們如何理解reentrant呢?
想象下以下代碼:
lock (locker) lock (locker) lock (locker) { // Do something... }
或者是:
Monitor.Enter(locker); Monitor.Enter(locker); Monitor.Enter(locker); // Do something... Monitor.Exit(locker); Monitor.Exit(locker); Monitor.Exit(locker);
這種情況下,只有在最後一個exit執行後,或者執行了相應次數的Exit後,locker才是可獲取的狀態。
Mutex像c#中的lock一樣,但是在不同的進程中仍然可以使用。換句話說,Mutex是一個計算機級別的鎖。因此獲取這樣一個鎖要比Monitor慢很多。
示例代碼:
using System; using System.Threading.Tasks; using System.Threading; namespace MultiThreadTest { class OneAtATimePlease { static void Main() { // Naming a Mutex makes it available computer-wide. Use a name that's // unique to your company and application (e.g., include your URL). using (var mutex = new Mutex(false, "oreilly.com OneAtATimeDemo")) { // Wait a few seconds if contended, in case another instance // of the program is still in the process of shutting down. if (!mutex.WaitOne(TimeSpan.FromSeconds(3), false)) { Console.WriteLine("Another app instance is running. Bye!"); return; } RunProgram(); } } static void RunProgram() { Console.WriteLine("Running. Press Enter to exit"); Console.ReadLine(); } } }
Monitor和Mutex都是排他鎖,Semaphore我們常用的另外一種非排他的鎖。
我們用它來實現這樣一個例子:一個酒吧,最多能容納3人,如果客滿則需要等待,有客人離開,等待的人隨時可以進來。
示例代碼:
using System; using System.Threading; class TheClub // No door lists! { static Semaphore _sem = new Semaphore(3, 3); // Capacity of 3 static void Main() { for (int i = 1; i <= 5; i++) new Thread(Enter).Start(i); Console.ReadLine(); } static void Enter(object id) { Console.WriteLine(id + " wants to enter"); _sem.WaitOne(); Console.WriteLine(id + " is in!"); // Only three threads Thread.Sleep(1000 * (int)id); // can be here at Console.WriteLine(id + " is leaving"); // a time. _sem.Release(); } }
使用Semaphore需要調用者來控制訪問資源,調用WaitOne來獲取資源,通過Release來釋放資源。開發者有責任確保資源能夠正確釋放。
Semaphore在限制同步訪問的時候非常有用,它不會像Monitor或者Mutex那樣當一個線程訪問某些資源時,其它所有線程都需要等,而是設置一個緩沖區,允許最多多少個線程同時進行訪問。
Semaphore也可以像Mutex一樣,跨進程進行同步。
本節主要總結了使用鎖進行同步,下一節將總結使用信號量進行同步。