0. Motivation
會想要寫這篇,主要是在公司測試產品系統時,發現之前有人在測試的時候沒有正常的關閉系統,導致同事想要測試產品的時候,出現系統重複執行的錯誤訊息,當中就有 互斥鎖(Mutex) 的文字出現,於是向我請教 「Mutex 是什麼?」的問題,其實使用 Mutex 就是一種避免程式被重複執行的方法,於是想在這邊寫一下有關這個的筆記。
下面的範例程式專案放在 : Synchronization_CSharp。
1. 並行、平行以及同步處理的淺談
提到 Mutex,就一定會提到同步處理(Synchronization),而講到同步處理就一定會想到並行(Concurrency)與平行(Parallelism)。
- 並行(Concurrency) : 一種程式的架構,將程式拆成多個可獨立運作的工作(Task)。
- 平行(Parallelism) : 指一種程式執行的方式,同時執行多個程式。
$\star$ : 將程式拆成 n 個 Task 後,要是同時運作,就可以說並行中用到平行,所以,不一定要平行才能並行。
下面為拾人牙慧的說明圖 :
那什麼是同步處理呢?
- 同步處理(Synchronization) : 確保多個執行單元(Process/Thread)同時存取某些資源的時候,執行結果不會因為執行單元的時間先後導致發生不可預期的錯誤。
當中常見的同步處理的方法有三種 : 互斥鎖(Mutex)、號誌(Semaphore)、自旋鎖(Spinlock)。
參看沒那麼淺的淺談 : Linux 核心設計: 淺談同步機制
2. Mutex
Mutual exclusion,縮寫 Mutex,是一種用於多執行緒編程中,防止兩條執行緒同時對同一公共資源(比如全域變數)進行讀寫的機制。該目的通過將代碼切片成一個一個的臨界區域(Critical Section)達成。臨界區域指的是一塊對公共資源進行存取的代碼,並非一種機制或是演算法。一個程式、行程、執行緒可以擁有多個臨界區域,但是並不一定會應用互斥鎖,更詳細資訊可參考這篇 : Synchronization (資料同步)。
簡單的偽程式碼說明 :
// 定義 Mutex 結構,表示一個互斥鎖
Mutex:
locked = false // 初始狀態:Mutex 為未鎖定
waitingQueue = empty // 初始化等待佇列,用來存放等待取得 Mutex 的執行緒
// 定義 acquire() 函式,讓執行緒嘗試進入 Critical Section
function acquire():
if locked == false then // 如果 Mutex 沒有被鎖定
locked = true // 則將 Mutex 設為鎖定,表示目前有執行緒進入 Critical Section
else // 否則(Mutex 已被鎖定)
add current_thread to waitingQueue // 將當前執行緒加入等待佇列
block current_thread // 阻塞當前執行緒,直到被喚醒以取得 Mutex
// 定義 release() 函式,讓執行緒離開 Critical Section並釋放 Mutex
function release():
if waitingQueue is not empty then // 如果等待佇列中有等待的執行緒
next_thread = remove first thread from waitingQueue // 移除等待佇列中的第一個執行緒
wake up next_thread // 喚醒該執行緒,讓它可以取得 Mutex
else // 如果等待佇列為空(沒有等待的執行緒)
locked = false // 則將 Mutex 設為未鎖定,供其他執行緒使用
2-1. C# 專案範例
專案說明 : 分成兩個檔案,分別為Mutex.cs
與Programs.cs
。範例中主執行緒先取得 Mutex 並占用 7 秒,副執行緒在 2 秒後開始每秒嘗試取得 Mutex,直到主執行緒釋放後,副執行緒成功取得 Mutex 並占用 5 秒,最後釋放 Mutex 並結束程式。
Mutex.cs
:
using System.Threading;
namespace MutexExample
{
// 此類別提供全域唯一的 Mutex 來保護 Critical Section
public static class MutexHelper
{
public static Mutex Mutex = new Mutex();
}
}
Programs.cs
:
using System;
using System.Threading;
namespace MutexExample
{
class Programs
{
static void Main(string[] args)
{
// 為主執行緒命名
Thread.CurrentThread.Name = "主執行緒";
Console.WriteLine($"{Thread.CurrentThread.Name} 嘗試進入 Critical Section...");
// 主執行緒進入 Critical Section
MutexHelper.Mutex.WaitOne();
Console.WriteLine($"{Thread.CurrentThread.Name} 已進入 Critical Section,將占用 7 秒...");
// 啟動副執行緒,讓其也嘗試進入 Critical Section
Thread worker = new Thread(WorkerThread);
worker.Name = "副執行緒";
worker.Start();
// 主執行緒在 Critical Section 中執行 7 秒的工作
Thread.Sleep(7000);
Console.WriteLine($"{Thread.CurrentThread.Name} 離開 Critical Section");
// 主執行緒離開 Critical Section
MutexHelper.Mutex.ReleaseMutex();
// 等待副執行緒完成
worker.Join();
Console.WriteLine("程式結束");
}
static void WorkerThread()
{
// 若執行緒尚未命名則命名(通常已在建立時指定)
if (Thread.CurrentThread.Name == null)
{
Thread.CurrentThread.Name = "副執行緒";
}
// 等待 2 秒後開始嘗試進入 Critical Section
Thread.Sleep(2000);
Console.WriteLine($"\n{Thread.CurrentThread.Name}:2 秒後開始嘗試進入 Critical Section...");
// 額外等待 1 秒,讓輸出順序更清楚
Thread.Sleep(1000);
while (true)
{
// 嘗試以非阻塞方式取得 Mutex (timeout = 0)
if (MutexHelper.Mutex.WaitOne(0))
{
Console.WriteLine($"{Thread.CurrentThread.Name} 成功進入 Critical Section,將占用 5 秒...");
// 模擬在 Critical Section 中執行 5 秒的工作
Thread.Sleep(5000);
Console.WriteLine($"{Thread.CurrentThread.Name} 離開 Critical Section");
// 離開 Critical Section
MutexHelper.Mutex.ReleaseMutex();
break;
}
else
{
Console.WriteLine($"{Thread.CurrentThread.Name} 正在等待進入 Critical Section...");
Thread.Sleep(1000);
}
}
}
}
}
3. Race Condition 問題
競爭條件(Race Condition)是指在多執行緒或多進程的環境中,當多個執行緒/行程(Thread/Process)在沒有適當同步機制保護的情況下,同時讀取或修改共享資源時,程式的最終結果會依賴於這些操作的執行順序或時機,從而可能導致不一致、不可預期或錯誤的結果。
3-1. C# 專案範例 (發生Race Condition)
專案說明 : 在此範例中,我們定義了一個全域共享資源sharedData
。四個執行緒各自讀取、增加、寫入這個值,且中間加入短暫延遲來放大競爭現象。
using System;
using System.Threading;
namespace RaceConditionExample
{
class Program
{
// 共享資源:計數器
static int sharedData = 0;
// 每個執行緒的迭代次數
static int iterations = 20;
static void Main(string[] args)
{
Thread[] threads = new Thread[4];
// 建立 4 個執行緒,共同執行 IncrementWithoutLock 方法
for (int i = 0; i < threads.Length; i++)
{
threads[i] = new Thread(IncrementWithoutLock);
threads[i].Name = $"Thread {i + 1}";
threads[i].Start();
}
// 等待所有執行緒完成
foreach (var thread in threads)
{
thread.Join();
}
// 預期值應為 4 * iterations,但 Race Condition 可能導致較低結果
Console.WriteLine($"\n最終 sharedData 值:{sharedData} (預期:{4 * iterations})");
}
static void IncrementWithoutLock()
{
for (int i = 0; i < iterations; i++)
{
// 讀取共享資源
int temp = sharedData;
Console.WriteLine($"{Thread.CurrentThread.Name} 讀取 sharedData = {temp}");
// 模擬計算:加 1
temp = temp + 1;
// 模擬延遲,容易讓執行緒交錯產生競爭
Thread.Sleep(10);
// 將計算後的值寫回共享資源
sharedData = temp;
Console.WriteLine($"{Thread.CurrentThread.Name} 寫入 sharedData = {sharedData}");
}
}
}
}
結果 :
3-2. C# 專案範例 (使用 Mutex 處理 Race Condition)
專案說明 : 在此範例中,我們利用Mutex
來保護對共享資源sharedData
的存取。在進行讀-加-寫操作前,先取得Mutex
鎖,確保同一時間只有一個執行緒可以進入臨界區執行,從而避免競爭問題。
using System;
using System.Threading;
namespace MutexSolutionExample
{
class Program
{
// 共享資源:計數器
static int sharedData = 0;
// 每個執行緒的迭代次數
static int iterations = 20;
// 用於保護 sharedData 的 Mutex 物件
static Mutex mutex = new Mutex();
static void Main(string[] args)
{
Thread[] threads = new Thread[4];
// 建立 4 個執行緒,共同執行 IncrementWithLock 方法
for (int i = 0; i < threads.Length; i++)
{
threads[i] = new Thread(IncrementWithLock);
threads[i].Name = $"Thread {i + 1}";
threads[i].Start();
}
// 等待所有執行緒完成
foreach (var thread in threads)
{
thread.Join();
}
// 所有執行緒正確同步後,最終值應為 4 * iterations
Console.WriteLine($"\n最終 sharedData 值:{sharedData} (預期:{4 * iterations})");
}
static void IncrementWithLock()
{
for (int i = 0; i < iterations; i++)
{
// 進入臨界區前先取得 Mutex
mutex.WaitOne();
// 讀取共享資源
int temp = sharedData;
Console.WriteLine($"{Thread.CurrentThread.Name} 讀取 sharedData = {temp}");
// 模擬計算:加 1
temp = temp + 1;
// 模擬延遲(在臨界區中也能確保操作完整性)
Thread.Sleep(10);
// 將計算後的值寫回共享資源
sharedData = temp;
Console.WriteLine($"{Thread.CurrentThread.Name} 寫入 sharedData = {sharedData}");
// 離開臨界區,釋放 Mutex
mutex.ReleaseMutex();
}
}
}
}
結果 :