C#多執行緒(4):進程同步Mutex類

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 ://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” 描述互斥體名稱相對於終端伺服器會話的作用域,而不是相對於進程。

請參考:

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

//www.cnblogs.com/suntp/p/8258488.html

接替運行

這裡要實現,當同時點擊一個程式時,只能有一個實例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()

進程同步示例

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

父進程 Parant 啟動子進程 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));
            }
        }
    }
}

然後發布或生成項目,打開程式文件位置,複製執行緒文件路徑。
創建一個新項目,名為 Parant 的 .NET Core 控制台,其 Program 中的程式碼如下:

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

namespace Parant
{
    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 項目的程式文件路徑,替換到 Parant 項目啟動子進程的那部分字元串中。

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

另外

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

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

另外打開互斥體,請參考

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

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

到目前為止,我們學習了排他鎖 lock、Monitor、Mutex。下一篇我們將來學習非排他鎖定結構的SemaphoreSemaphoreSlim

Tags: