Mutex 與 Race Condition 問題


Posted by Mephisto on 2025-02-16

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.csPrograms.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();
            }
        }
    }
}

結果 :


#同步處理 #互斥鎖 #競爭情況 #RaceCondition #mutex







Related Posts

前後端分離與 SPA

前後端分離與 SPA

[ js 筆記 ] this 是什麼東西?能吃嗎?

[ js 筆記 ] this 是什麼東西?能吃嗎?

JavaScript 五四三 Ep.03 Array.prototype.filter()

JavaScript 五四三 Ep.03 Array.prototype.filter()


Comments