C#多線程系列之進程同步Mutex類

Mutex 中文為互斥,Mutex 類叫做互斥鎖。它還可用於進程間同步的同步基元。

Mutex 跟 lock 相似,但是 Mutex 支持多個進程。Mutex 大約比 lock 慢 20 倍。

互斥鎖(Mutex),用於多線程中防止兩條線程同時對一個公共資源進行讀寫的機制。

Windows 操作系統中,Mutex 同步對象有兩個狀態:

  • signaled:未被任何對象擁有;
  • nonsignaled:被一個線程擁有;

Mutex 隻能在獲得鎖的線程中,釋放鎖。

構造函數和方法

Mutex 類其構造函數如下:

構造函數 說明
Mutex() 使用默認屬性初始化 Mutex類的新實例。
Mutex(Boolean) 使用 Boolean 值(指示調用線程是否應具有互斥體的初始所有權)初始化 Mutex 類的新實例。
Mutex(Boolean, String) 使用 Boolean 值(指示調用線程是否應具有互斥體的初始所有權以及字符串是否為互斥體的名稱)初始化 Mutex 類的新實例。
Mutex(Boolean, String, Boolean) 使用可指示調用線程是否應具有互斥體的初始所有權以及字符串是否為互斥體的名稱的 Boolean 值和當線程返回時可指示調用線程是否已賦予互斥體的初始所有權的 Boolean 值初始化 Mutex 類的新實例。

Mutex 對於進程同步有所幫助,例如其應用場景主要是控制系統隻能運行一個此程序的實例。

Mutex 構造函數中的 String類型參數 叫做互斥量而互斥量是全局的操作系統對象。 
Mutex 隻要考慮實現進程間的同步,它會耗費比較多的資源,進程內請考慮 Monitor/lock。

Mutex 的常用方法如下:

方法 說明
Close() 釋放由當前 WaitHandle 占用的所有資源。
Dispose() 釋放由 WaitHandle 類的當前實例占用的所有資源。
OpenExisting(String) 打開指定的已命名的互斥體(如果已經存在)。
ReleaseMutex() 釋放 Mutex一次。
TryOpenExisting(String, Mutex) 打開指定的已命名的互斥體(如果已經存在),並返回指示操作是否成功的值。
WaitOne() 阻止當前線程,直到當前 WaitHandle 收到信號。
WaitOne(Int32) 阻止當前線程,直到當前 WaitHandle 收到信號,同時使用 32 位帶符號整數指定時間間隔(以毫秒為單位)。
WaitOne(Int32, Boolean) 阻止當前線程,直到當前的 WaitHandle 收到信號為止,同時使用 32 位帶符號整數指定時間間隔,並指定是否在等待之前退出同步域。
WaitOne(TimeSpan) 阻止當前線程,直到當前實例收到信號,同時使用 TimeSpan 指定時間間隔。
WaitOne(TimeSpan, Boolean) 阻止當前線程,直到當前實例收到信號為止,同時使用 TimeSpan 指定時間間隔,並指定是否在等待之前退出同步域。

關於 Mutex 類,我們可以先通過幾個示例去瞭解它。

系統隻能運行一個程序的實例

下面是一個示例,用於控制系統隻能運行一個此程序的實例,不允許同時啟動多次。

    class Program
    {
        // 第一個程序
        const string name = "www.whuanle.cn";
        private static Mutex m;
        static void Main(string[] args)
        {
            // 本程序是否是 Mutex 的擁有者
            bool firstInstance;
            m = new Mutex(false,name,out firstInstance);
            if (!firstInstance)
            {
                Console.WriteLine("程序已在運行!按下回車鍵退出!");
                Console.ReadKey();
                return;
            }
            Console.WriteLine("程序已經啟動");
            Console.WriteLine("按下回車鍵退出運行");
            Console.ReadKey();
            m.ReleaseMutex();
            m.Close();
            return;
        }
    }

上面的代碼中,有些地方前面沒有講,沒關系,我們運行一下生成的程序先。

解釋一下上面的示例

Mutex 的工作原理:

當兩個或兩個以上的線程同時訪問共享資源時,操作系統需要一個同步機制來確保每次隻有一個線程使用資源。

Mutex 是一種同步基元,Mutex 僅向一個線程授予獨占訪問共享資源的權限。這個權限依據就是 互斥體,當一個線程獲取到互斥體後,其它線程也在試圖獲取互斥體時,就會被掛起(阻塞),直到第一個線程釋放互斥體。

對應我們上一個代碼示例中,實例化 Mutex 類的構造函數如下:

m = new Mutex(false,name,out firstInstance);

其構造函數原型如下:

public Mutex (bool initiallyOwned, string name, out bool createdNew);

前面我們提出過,Mutex 對象有兩種狀態,signaled 和 nonsignaled。

通過 new 來實例化 Mutex 類,會檢查系統中此互斥量 name 是否已經被使用,如果沒有被使用,則會創建 name 互斥量並且此線程擁有此互斥量的使用權;此時 createdNew == true

那麼 initiallyOwned ,它的作用是是否允許線程是否能夠獲取到此互斥量的初始化所有權。因為我們希望隻有一個程序能夠在後臺運行,因此我們要設置為 false。

驅動開發中關於Mutex :https://docs.microsoft.com/zh-cn/windows-hardware/drivers/kernel/introduction-to-mutex-objects

對瞭, Mutex 的 參數中,name 是非常有講究的。

在運行終端服務的服務器上,命名系統 mutex 可以有兩個級別的可見性。

  • 如果其名稱以前綴 "Global" 開頭,則 mutex 在所有終端服務器會話中可見。
  • 如果其名稱以前綴 "Local" 開頭,則 mutex 僅在創建它的終端服務器會話中可見。 在這種情況下,可以在服務器上的其他每個終端服務器會話中存在具有相同名稱的單獨 mutex。

如果在創建已命名的 mutex 時未指定前綴,則采用前綴 "Local"。 在終端服務器會話中,兩個互斥體的名稱隻是它們的前綴不同,它們都是對終端服務器會話中的所有進程都可見。

也就是說,前綴名稱 "Global" 和 "Local" 描述互斥體名稱相對於終端服務器會話的作用域,而不是相對於進程。

請參考:

https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex?view=netcore-3.1#methods

https://www.jb51.net/article/237313.htm

接替運行

這裡要實現,當同時點擊一個程序時,隻能有一個實例A可以運行,其它實例進入等待隊列,等待A運行完畢後,然後繼續運行隊列中的下一個實例。

我們將每個程序比作一個人,模擬一個廁所坑位,每次隻能有一個人上廁所,其他人需要排隊等候。

使用 WaitOne() 方法來等待別的進程釋放互斥量,即模擬排隊;ReleaseMutex() 方法解除對坑位的占用。

    class Program
    {
        // 第一個程序
        const string name = "www.whuanle.cn";
        private static Mutex m;
        static void Main(string[] args)
        {
            // wc 還有沒有位置
            bool firstInstance;
            m = new Mutex(true,name,out firstInstance);

            // 已經有人在上wc
            if (!firstInstance)
            {
                // 等待運行的實例退出,此進程才能運行。
                Console.WriteLine("排隊等待");
                m.WaitOne();
                GoWC();
                return;
            }
            GoWC();

            return;
        }

        private static void GoWC()
        {
            Console.WriteLine(" 開始上wc");
            Thread.Sleep(1000);
            Console.WriteLine(" 開門");
            Thread.Sleep(1000);
            Console.WriteLine(" 關門");
            Thread.Sleep(1000);
            Console.WriteLine(" xxx");
            Thread.Sleep(1000);
            Console.WriteLine(" 開門");
            Thread.Sleep(1000);
            Console.WriteLine(" 離開wc");
            m.ReleaseMutex();
            Thread.Sleep(1000);
            Console.WriteLine(" 洗手");
        }
    }

此時,我們使用瞭

            m = new Mutex(true,name,out firstInstance);

一個程序結束後,要允許其它線程能夠創建 Mutex 對象獲取互斥量,需要將構造函數的第一個參數設置為 true。

你也可以改成 false,看看會報什麼異常。

你可以使用 WaitOne(Int32) 來設置等待時間,單位是毫秒,超過這個時間就不排隊瞭,去別的地方上廁所。

為瞭避免出現問題,請考慮在 finally 塊中執行 m.ReleaseMutex()

進程同步示例

這裡我們實現一個這樣的場景:

父進程 Parent 啟動子進程 Children ,等待子進程 Children 執行完畢,子進程退出,父進程退出。

新建一個 .NET Core 控制臺項目,名稱為 Children,其 Progarm 中的代碼如下

using System;
using System.Threading;

namespace Children
{
    class Program
    {
        const string name = "進程同步示例";
        private static Mutex m;
        static void Main(string[] args)
        {
            Console.WriteLine("子進程被啟動...");
            bool firstInstance;

            // 子進程創建互斥體
            m = new Mutex(true, name, out firstInstance);

            // 按照我們設計的程序,創建一定是成功的
            if (firstInstance)
            {
                Console.WriteLine("子線程執行任務");
                DoWork();
                Console.WriteLine("子線程任務完成");

                // 釋放互斥體
                m.ReleaseMutex();
                // 結束程序
                return;
            }
            else
            {
                Console.WriteLine("莫名其妙的異常,直接退出");
            }
        }
        private static void DoWork()
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine("子線程工作中");
                Thread.Sleep(TimeSpan.FromSeconds(1));
            }
        }
    }
}

然後發佈或生成項目,打開程序文件位置,復制線程文件路徑。
創建一個新項目,名為 Parent 的 .NET Core 控制臺,其 Program 中的代碼如下:

using System;
using System.Diagnostics;
using System.Threading;

namespace Parent
{
    class Program
    {
        const string name = "進程同步示例";
        private static Mutex m;
        static void Main(string[] args)
        {
            // 晚一些再執行,我錄屏要對正窗口位置
            Thread.Sleep(TimeSpan.FromSeconds(3));
            Console.WriteLine("父進程啟動!");

            new Thread(() =>
            {
                // 啟動子進程
                Process process = new Process();
                process.StartInfo.UseShellExecute = true;
                process.StartInfo.CreateNoWindow = false;
                process.StartInfo.WorkingDirectory = @"../../../ConsoleApp9\Children\bin\Debug\netcoreapp3.1";
                process.StartInfo.FileName = @"../../../ConsoleApp9\Children\bin\Debug\netcoreapp3.1\Children.exe";
                process.Start();
                process.WaitForExit();
            }).Start();


            // 子進程啟動需要一點時間
            Thread.Sleep(TimeSpan.FromSeconds(1));

            // 獲取互斥體
            bool firstInstance;
            m = new Mutex(true, name, out firstInstance);

            // 說明子進程還在運行
            if (!firstInstance)
            {
                // 等待子進程運行結束
                Console.WriteLine("等待子進程運行結束");
                m.WaitOne();
                Console.WriteLine("子進程運行結束,程序將在3秒後自動退出");
                m.ReleaseMutex();
                Thread.Sleep(TimeSpan.FromSeconds(3));
                return;
            }
        }
    }
}

請將 Children 項目的程序文件路徑,替換到 Parent 項目啟動子進程的那部分字符串中。

然後啟動 Parent.exe,可以觀察到如下圖的運行過程:

另外

構造函數中,如果為 name 指定 null 或空字符串,則將創建一個本地 Mutex 對象,隻會在進程內有效。

Mutex 有些使用方法比較隱晦,可以參考 https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.-ctor?view=netcore-3.1#System_Threading_Mutex__ctor_System_Boolean_

另外打開互斥體,請參考

https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.openexisting?view=netcore-3.1

https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.tryopenexisting?view=netcore-3.1

到此這篇關於C#多線程系列之進程同步Mutex類的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: