1. 介紹
回想上一堂課的例子:在我們還很簡單的應用程式裡兩個執行緒會去遞增同一個計數器,但最終值不一定是預期的。為了保護這個計數器我們已經用過 keyword lock(或更精確地說是 Monitor),它適合用在單一進程內的同步。但如果你的程式不是唯一一個想使用資源的呢?例如你寫了一個服務,跑了兩個實例,兩個都想寫同一個檔案…或要共用某個硬體設備,比方說印表機埠?這時候就輪到老朋友互斥量(mutex,來自 mutual exclusion — 「互斥」)上場了。
概念
互斥量是一種同步原語,不僅限制同一進程內不同執行緒對資源的存取,還能協調同一台機器上不同進程之間的存取。把它想像成會掛在會議室門口的大牌子「占用中」,公司員工和來訪者都能看到它。
在 .NET 中對應的類別是 System.Threading.Mutex。
什麼時候你真的需要 Mutex:
- 當你需要在不同進程之間同步存取(例如,兩個獨立的應用程式在操作同一個檔案)。
- 當資源非常貴重且不可分割,以至於即便是進程的上下文也不能共享存取權。
如果只是單一進程內的執行緒間同步,通常用 lock(Monitor)。因為 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("臨界區已釋放。");
}
}
}
}
試試看:
- 打開兩個視窗執行這個應用程式。
- 都啟動 — 第二個會等第一個按下 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
| 原語 | 支援跨進程同步? | 速度 | 使用場景 |
|---|---|---|---|
|
否 | 非常快 | 同一進程內的執行緒之間 |
|
是 | 較慢(較昂貴) | 不同進程之間的執行緒 |
|
是(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(這通常表示有錯誤或資源處於不一致狀態)。
GO TO FULL VERSION