.NET 中使用 Mutex 進行跨越進程邊界的同步

Mutex 是 Mutual Exclusion 的縮寫,是互斥鎖,用於防止兩個線程同時對計算機上的同一個資源進行訪問。不過相比於其他互斥的方式,Mutex 能夠跨越線程邊界。


 

 

Mutex 是什麼?

與其他線程同步的方式一樣,Mutex 也提供對資源的互斥訪問;不過 Mutex 使用的系統資源會比 Monitor 更多,而 Monitor 就是實現 C# 中 lock 關鍵字所用的鎖。

用更多的系統資源,帶來更強大的功能 —— Mutex 能進行跨越應用程序域邊界的封送,能進行跨越進程邊界的線程同步。

簡單的 Mutex(不能跨進程互斥)

最簡單的 Mutex 的使用方法就是直接 new 出來,然後使用 Wait 進行等待,使用 ReleaseMutex 進行釋放。

private readonly Mutex _mutex = new Mutex();

private void UseResource()
{
    _mutex.WaitOne();
    
    // 等待一小段時間,假裝正在使用公共資源。這裏的一段代碼在單個進程之內將無法重入。
    Thread.Sleep(500);

    _mutex.ReleaseMutex();
}

參數中有一個 initiallyOwned 參數,如果指定爲 true 表示創建這個 Mutex 的線程擁有這個資源(不需要等待),當這個線程調用 ReleaseMutex 之後其他線程的 WaitOne 纔會生效。

不過這種方式不能達到跨進程同步的效果,所以實際上本文並不會過多描述這種互斥方式。

創建跨進程互斥的 Mutex

要創建跨進程互斥的 Mutex,必須要給 Mutex 指定名稱。

使用 new Mutex(false, "Walterlv.Mutex") 創建一個命名的互斥鎖,以便進行跨進程的資源互斥訪問。

在使用這個構造函數重載的時候,第一個參數 initiallyOwned 建議的取值爲 false。因爲當你指定爲 true 時,說明你希望此線程是初始創建此 Mutex 的線程,然而由於你是直接 new 出來的,所以你實質上是無法得知你到底是不是第一個 new 出來的。

class Program
{
    static async Task Main(string[] args)
    {
        var program = new Program();
        while (true)
        {
            // 不斷地嘗試訪問一段資源。這樣,當多個進程運行的時候,可以很大概率模擬出現資源訪問衝突。
            program.UseResource();
            await Task.Delay(50);
        }
    }


    private void UseResource()
    {
        var mutex = new Mutex(false, "Walterlv.Mutex");
        mutex.WaitOne();

        // 正在使用公共資源。
        // 這裏的一段代碼將無法重入,即使是兩個不同的進程。
        var path = @"C:\Users\lvyi\Desktop\walterlv.log";
        Console.WriteLine($"[{DateTime.Now:O}] 開始寫入文件……");
        File.AppendAllText(path, $"[{DateTime.Now:O}] 開始寫入文件……", Encoding.UTF8);
        Thread.Sleep(1000);
        File.AppendAllText(path, $"[{DateTime.Now:O}] 寫入文件完成。", Encoding.UTF8);
        Console.WriteLine($"[{DateTime.Now:O}] 寫入文件完成。");

        mutex.ReleaseMutex();
    }
}

注意此程序在兩個進程下的運行效果,明明我們等待使用資源的時間間隔只有 50 ms,但實際上等待時間是 1000 ms 左右。在關掉其中一個進程之後,間隔恢復到了 50 ms 左右。

這說明 Mutex 的等待在這裏起到了跨進程互斥的作用。

以上代碼在兩個進程下的運行結果

當你需要在是否是第一次創建出來的時候進行一些特殊處理,就使用帶 createdNew 參數的構造函數。

    private void UseResource()
    {
--      var mutex = new Mutex(false, "Walterlv.Mutex");
++      var mutex = new Mutex(true, "Walterlv.Mutex", out var createdNew);

--      mutex.WaitOne();
++      // 如果這個 Mutex 是由此處創建出來的,即 createdNew 爲 true,說明第一個參數 initiallyOwned 是真的發生了,於是我們就不需要等待。
++      // 反之,當 createdNew 爲 false 的時候,說明已經有一個現成的 Mutex 已經存在,我們在這裏需要等待。
++      if (!createdNew)
++      {
++          mutex.WaitOne();
++      }
        ……
        mutex.ReleaseMutex();
    }

處理異常情況

ApplicationException

mutex.ReleaseMutex(); 方法只能被當前擁有它的線程調用,如果某個線程試圖調用這個函數,卻沒有擁有這個 Mutex,就會拋出 ApplicationException

怎樣爲擁有呢?還記得前面構造函數中的 initiallyOwned 參數嗎?就是在指定自己是否是此 Mutex 的擁有者的(實際上我們還需要使用 createdNew 來輔助驗證這一點)。

當一個線程沒有擁有這個 Mutex 的時候,需要使用 WaitOne 來等待獲得這個鎖。

AbandonedMutexException

class Program
{
    static async Task Main(string[] args)
    {
        // 開啓一個線程,在那個線程中丟掉獲得的 Mutex。
        var thread = new Thread(AbandonMutex);
        thread.Start();

        // 不要讓進程退出,否則 Mutex 就會被系統回收。
        Console.Read();
    }

    private static void AbandonMutex()
    {
        // 獲得一個 Mutex,然後就不再釋放了。
        // 由於此線程會在 WaitOne 執行結束後退出,所以這個 Mutex 就被丟掉了。
        var mutex = new Mutex(false, "Walterlv.Mutex");
        mutex.WaitOne();
    }
}

上面的這段代碼,當你第一次運行此進程並且保持此進程不退出的時候並沒有什麼異樣。但是你再啓動第二個進程實例的話,就會在 WaitOne 那裏收到一個異常 —— AbandonedMutexException

所以如果你不能在一處代碼中使用 try-finally 來確保在獲得鎖之後一定會釋放的話,那麼強烈建議在 WaitOne 的時候捕獲異常。順便提醒,try-finally 中不能有異步代碼,你可以參見:在有 UI 線程參與的同步鎖(如 AutoResetEvent)內部使用 await 可能導致死鎖

也就是說,當你需要等待的時候,catch 一下異常。在 catch 完之後,你並不需要再次使用 WaitOne 來等待,因爲即便發生了異常,你也依然獲得了鎖。這一點你可以通過調用 ReleaseMutex 來驗證,因爲前面我們說了只有擁有鎖的線程纔可以釋放鎖。

private static void WaitOne()
{
    var mutex = new Mutex(false, "Walterlv.Mutex");
    try
    {
        mutex.WaitOne();
    }
    catch (AbandonedMutexException ex)
    {
        Console.WriteLine("發現被遺棄的鎖");
    }
    Console.WriteLine("獲得了鎖");
}

參考資料


我的博客會首發於 https://walterlv.com/,而 CSDN 和博客園僅從其中摘選發佈,而且一旦發佈了就不再更新。

知識共享許可協議

本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名呂毅(包含鏈接:https://blog.csdn.net/wpwalter),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。如有任何疑問,請與我聯繫

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章