CodeGym /課程 /C# SELF /用來同步的互斥量: Mutex

用來同步的互斥量: Mutex

C# SELF
等級 56 , 課堂 2
開放

1. 介紹

回想上一堂課的例子:在我們還很簡單的應用程式裡兩個執行緒會去遞增同一個計數器,但最終值不一定是預期的。為了保護這個計數器我們已經用過 keyword lock(或更精確地說是 Monitor),它適合用在單一進程內的同步。但如果你的程式不是唯一一個想使用資源的呢?例如你寫了一個服務,跑了兩個實例,兩個都想寫同一個檔案…或要共用某個硬體設備,比方說印表機埠?這時候就輪到老朋友互斥量(mutex,來自 mutual exclusion — 「互斥」)上場了。

概念

互斥量是一種同步原語,不僅限制同一進程內不同執行緒對資源的存取,還能協調同一台機器上不同進程之間的存取。把它想像成會掛在會議室門口的大牌子「占用中」,公司員工和來訪者都能看到它。

在 .NET 中對應的類別是 System.Threading.Mutex

什麼時候你真的需要 Mutex

  • 當你需要在不同進程之間同步存取(例如,兩個獨立的應用程式在操作同一個檔案)。
  • 當資源非常貴重且不可分割,以至於即便是進程的上下文也不能共享存取權。

如果只是單一進程內的執行緒間同步,通常用 lockMonitor)。因為 Mutex 較重且較慢,所以更適合用於跨進程同步。

Mutex 的工作方式

flowchart TD
    A(進程 1) --|請求|--> M(Mutex)
    B(進程 2) --|請求|--> M
    M --|只允許一個|--> R(共用資源)
    A --|釋放|--> M
    B --|在釋放後|--> M

2. 使用 Mutex 的基本語法

建立

Mutex 的建立跟大多數同步類別一樣簡單:

using System.Threading;

Mutex mutex = new Mutex();

主要方法

  • WaitOne() — 嘗試取得互斥量;如果取不到,執行緒會被阻塞直到別人釋放。
  • ReleaseMutex() — 釋放互斥量,允許其他執行緒或進程進入临界區。

最簡單的例子:在同一進程內不同執行緒的同步

using System;
using System.Threading;

class Program
{
    static Mutex mutex = new Mutex();

    static void Main()
    {
        Thread t1 = new Thread(PrintNumbers);
        Thread t2 = new Thread(PrintNumbers);
        t1.Start();
        t2.Start();
        t1.Join();
        t2.Join();
    }

    static void PrintNumbers()
    {
        for (int i = 0; i < 5; i++)
        {
            mutex.WaitOne(); // 進入臨界區
            Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: {i}");
            mutex.ReleaseMutex(); // 離開臨界區
            Thread.Sleep(100); // 讓行為更明顯
        }
    }
}

在這個例子中,兩個執行緒會輪流取得控制權,把資料列印到主控台。

3. 使用命名互斥量進行跨進程同步

要做真正的「重型」跨進程同步,需要用到 命名 互斥量。你給它一個名字,系統上所有進程都能以這個名字存取同一個互斥量。

Mutex mutex = new Mutex(false, "MyApp_Mutex");

建構子參數:

  • 第一個參數(bool initiallyOwned)— 是否建立時就由當前執行緒立刻取得互斥量。通常為 false
  • 第二個參數— 互斥量的名字。使用相同名字的所有進程會指向同一個系統資源,例如 "MyApp_Mutex"

範例:兩個應用程式共用一個 Mutex

從兩個不同視窗啟動相同程式可以看到效果。

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        using (Mutex mutex = new Mutex(false, "MySuperUniqueMutexName"))
        {
            Console.WriteLine("嘗試進入臨界區...");
            mutex.WaitOne(); // 等待,直到別的進程釋放互斥量
            try
            {
                Console.WriteLine("臨界區由此進程佔用。");
                Console.WriteLine("按 Enter 離開臨界區。");
                Console.ReadLine();
            }
            finally
            {
                mutex.ReleaseMutex();
                Console.WriteLine("臨界區已釋放。");
            }
        }
    }
}

試試看:

  1. 打開兩個視窗執行這個應用程式。
  2. 都啟動 — 第二個會等第一個按下 Enter 才能進入。

4. 限制同時執行的應用程式實例數

Mutex 常用於限制同一程式的同時執行個數。例如:「喂,用戶,下次別開兩個計算機副本!」之類的場景。

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        bool createdNew;
        using (Mutex mutex = new Mutex(true, "CalculatorAppInstanceMutex", out createdNew))
        {
            if (!createdNew)
            {
                Console.WriteLine("應用程式已經在執行!");
                return;
            }

            Console.WriteLine("應用程式已成功啟動。按 Enter 退出。");
            Console.ReadLine();
        }
    }
}

這個模式在桌面應用中很常見:第一個執行個體正常啟動,第二個會提示並退出。Microsoft 的官方範例可見於 文件

5. 使用 Mutex 的常見錯誤

錯誤 #1: 忘記呼叫 ReleaseMutex().
如果執行緒成功取得了互斥量(呼叫了 WaitOne()),但沒有呼叫 ReleaseMutex()(可能因為發生例外或忘記),其他執行緒或進程都無法進入,直到那個「忘記」的執行緒結束。這可能導致 deadlock(死鎖)。良好做法是總是用 try-finally

mutex.WaitOne();
try
{
    // 臨界區
}
finally
{
    mutex.ReleaseMutex();
}

錯誤 #2: 呼叫 ReleaseMutex() 的次數不正確。
如果比 WaitOne() 呼叫的次數更多地呼叫 ReleaseMutex(),會拋出 ApplicationException

錯誤 #3: 嘗試釋放不是自己擁有的 Mutex.
互斥量綁定到取得它的執行緒。只有那個執行緒才有權呼叫 ReleaseMutex()。如果其他執行緒嘗試釋放,.NET 會拋出例外。

錯誤 #4: 在只需要 lock 的情況下使用 Mutex.
Mutex 比一般的 lock 慢,因為它支援跨進程並需要系統呼叫。建議:若不需要跨進程同步,還是用 lock

錯誤 #5: 為互斥量取了不夠獨特的名字。
如果名字太簡單(例如 "MyMutex"),可能會不小心跟別人的程式撞名。最好用包含公司名或應用名的唯一名字。

6. 有用的細節

WaitOne(timeout)

你可以指定等待互斥量的逾時時間:

if (mutex.WaitOne(5000)) // 等最多 5 秒
{
    try { /* ... */ }
    finally { mutex.ReleaseMutex(); }
}
else
{
    Console.WriteLine("在 5 秒內無法取得資源!");
}

Mutex.TryOpenExisting

如果想連接到已存在的命名互斥量,使用靜態方法:

if (Mutex.TryOpenExisting("DiaryFileWriteMutex", out Mutex existingMutex))
{
    // 現在 existingMutex 是指向已存在互斥量的參考
}

不同使用者之間的存取

預設情況下,命名互斥量對所有有建立系統物件權限的使用者可見。如果需要更嚴格的控制,可以使用帶有 MutexSecurity 的建構子來設定存取權限。

比較表:lock/Monitor, Mutex, Semaphore

原語 支援跨進程同步? 速度 使用場景
lock/Monitor
非常快 同一進程內的執行緒之間
Mutex
較慢(較昂貴) 不同進程之間的執行緒
Semaphore
是(NamedSemaphore) 與 Mutex 相近 需要限制同時進入的執行緒數量時

關於 Semaphore 的更多內容會在下一堂課介紹。

Mutex 的狀態

stateDiagram-v2
    [*] --> Unowned
    Unowned --> Owned : WaitOne()
    Owned --> Owned : WaitOne() (reentrant)
    Owned --> Unowned : ReleaseMutex()
    Owned --> Abandoned : 執行緒「掛掉」沒有呼叫 ReleaseMutex()
    Abandoned --> Unowned
  • Unowned — 沒有人擁有互斥量。
  • Owned — 互斥量被某個執行緒擁有。
  • Abandoned — 執行緒意外結束而沒有呼叫 ReleaseMutex()。下一個取得互斥量的人會收到 AbandonedMutexException(這通常表示有錯誤或資源處於不一致狀態)。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION