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

痴者工良發表於2020-04-18

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("程式已在執行!按下Enter鍵退出!");
                Console.ReadKey();
                return;
            }
            Console.WriteLine("程式已經啟動");
            Console.WriteLine("按下Enter鍵退出執行");
            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.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 有些使用方法比較隱晦,可以參考 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

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

相關文章