轉載至:.NET 中使用 Mutex 進行跨越進程邊界的同步html
Mutex 是 Mutual Exclusion 的縮寫,是互斥鎖,用於防止兩個線程同時對計算機上的同一個資源進行訪問。不過相比於其餘互斥的方式,Mutex 可以跨越線程邊界。api
與其餘線程同步的方式同樣,Mutex 也提供對資源的互斥訪問;不過 Mutex 使用的系統資源會比 Monitor 更多,而 Monitor 就是實現 C# 中 lock 關鍵字所用的鎖。app
用更多的系統資源,帶來更強大的功能 —— Mutex 能進行跨越應用程序域邊界的封送,能進行跨越進程邊界的線程同步。異步
最簡單的 Mutex 的使用方法就是直接 new
出來,而後使用 Wait
進行等待,使用 ReleaseMutex
進行釋放。async
1 private readonly Mutex _mutex = new Mutex(); 2 3 private void UseResource() 4 { 5 _mutex.WaitOne(); 6 7 // 等待一小段時間,僞裝正在使用公共資源。這裏的一段代碼在單個進程以內將沒法重入。 8 Thread.Sleep(500); 9 10 _mutex.ReleaseMutex(); 11 }
參數中有一個 initiallyOwned
參數,若是指定爲 true
表示建立這個 Mutex 的線程擁有這個資源(不須要等待),當這個線程調用 ReleaseMutex
以後其餘線程的 WaitOne
纔會生效。函數
不過這種方式不能達到跨進程同步的效果,因此實際上本文並不會過多描述這種互斥方式。post
要建立跨進程互斥的 Mutex,必需要給 Mutex 指定名稱。ui
使用 new Mutex(false, "Walterlv.Mutex")
建立一個命名的互斥鎖,以便進行跨進程的資源互斥訪問。spa
在使用這個構造函數重載的時候,第一個參數 initiallyOwned
建議的取值爲 false
。由於當你指定爲 true
時,說明你但願此線程是初始建立此 Mutex
的線程,然而因爲你是直接 new
出來的,因此你實質上是沒法得知你究竟是不是第一個 new
出來的。線程
1 class Program 2 { 3 static async Task Main(string[] args) 4 { 5 var program = new Program(); 6 while (true) 7 { 8 // 不斷地嘗試訪問一段資源。這樣,當多個進程運行的時候,能夠很大機率模擬出現資源訪問衝突。 9 program.UseResource(); 10 await Task.Delay(50); 11 } 12 } 13 14 15 private void UseResource() 16 { 17 var mutex = new Mutex(false, "Walterlv.Mutex"); 18 mutex.WaitOne(); 19 20 // 正在使用公共資源。 21 // 這裏的一段代碼將沒法重入,即便是兩個不一樣的進程。 22 var path = @"C:\Users\lvyi\Desktop\walterlv.log"; 23 Console.WriteLine($"[{DateTime.Now:O}] 開始寫入文件……"); 24 File.AppendAllText(path, $"[{DateTime.Now:O}] 開始寫入文件……", Encoding.UTF8); 25 Thread.Sleep(1000); 26 File.AppendAllText(path, $"[{DateTime.Now:O}] 寫入文件完成。", Encoding.UTF8); 27 Console.WriteLine($"[{DateTime.Now:O}] 寫入文件完成。"); 28 29 mutex.ReleaseMutex(); 30 } 31 }
注意此程序在兩個進程下的運行效果,明明咱們等待使用資源的時間間隔只有 50 ms,但實際上等待時間是 1000 ms 左右。在關掉其中一個進程以後,間隔恢復到了 50 ms 左右。
這說明 Mutex 的等待在這裏起到了跨進程互斥的做用。
當你須要在是不是第一次建立出來的時候進行一些特殊處理,就使用帶 createdNew
參數的構造函數。
1 private void UseResource() 2 { 3 -- var mutex = new Mutex(false, "Walterlv.Mutex"); 4 ++ var mutex = new Mutex(true, "Walterlv.Mutex", out var createdNew); 5 6 -- mutex.WaitOne(); 7 ++ // 若是這個 Mutex 是由此處建立出來的,即 createdNew 爲 true,說明第一個參數 initiallyOwned 是真的發生了,因而咱們就不須要等待。 8 ++ // 反之,當 createdNew 爲 false 的時候,說明已經有一個現成的 Mutex 已經存在,咱們在這裏須要等待。 9 ++ if (!createdNew) 10 ++ { 11 ++ mutex.WaitOne(); 12 ++ } 13 …… 14 mutex.ReleaseMutex(); 15 }
mutex.ReleaseMutex();
方法只能被當前擁有它的線程調用,若是某個線程試圖調用這個函數,卻沒有擁有這個 Mutex,就會拋出 ApplicationException
。
怎樣爲擁有呢?還記得前面構造函數中的 initiallyOwned
參數嗎?就是在指定本身是不是此 Mutex 的擁有者的(實際上咱們還須要使用 createdNew
來輔助驗證這一點)。
當一個線程沒有擁有這個 Mutex 的時候,須要使用 WaitOne
來等待得到這個鎖。
1 class Program 2 { 3 static async Task Main(string[] args) 4 { 5 // 開啓一個線程,在那個線程中丟掉得到的 Mutex。 6 var thread = new Thread(AbandonMutex); 7 thread.Start(); 8 9 // 不要讓進程退出,不然 Mutex 就會被系統回收。 10 Console.Read(); 11 } 12 13 private static void AbandonMutex() 14 { 15 // 得到一個 Mutex,而後就再也不釋放了。 16 // 因爲此線程會在 WaitOne 執行結束後退出,因此這個 Mutex 就被丟掉了。 17 var mutex = new Mutex(false, "Walterlv.Mutex"); 18 mutex.WaitOne(); 19 } 20 }
上面的這段代碼,當你第一次運行此進程而且保持此進程不退出的時候並無什麼異樣。可是你再啓動第二個進程實例的話,就會在 WaitOne
那裏收到一個異常 —— AbandonedMutexException
。
因此若是你不能在一處代碼中使用 try-finally
來確保在得到鎖以後必定會釋放的話,那麼強烈建議在 WaitOne
的時候捕獲異常。順便提醒,try-finally
中不能有異步代碼,你能夠參見:在有 UI 線程參與的同步鎖(如 AutoResetEvent)內部使用 await 可能致使死鎖。
也就是說,當你須要等待的時候,catch
一下異常。在 catch
完以後,你並不須要再次使用 WaitOne
來等待,由於即使發生了異常,你也依然得到了鎖。這一點你能夠經過調用 ReleaseMutex
來驗證,由於前面咱們說了只有擁有鎖的線程才能夠釋放鎖。
1 private static void WaitOne() 2 { 3 var mutex = new Mutex(false, "Walterlv.Mutex"); 4 try 5 { 6 mutex.WaitOne(); 7 } 8 catch (AbandonedMutexException ex) 9 { 10 Console.WriteLine("發現被遺棄的鎖"); 11 } 12 Console.WriteLine("得到了鎖"); 13 }
參考資料
本文會常常更新,請閱讀原文: https://blog.walterlv.com/post/mutex-in-dotnet.html ,以免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。