首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

多執行緒程式設計思維深度解析:從 Optex 鎖實作看並發控制核心

     編輯:關於.NET

前言

雖然在專案中頻繁使用多執行緒,但閱讀 Jeffrey Richter 關於鎖的文章後發現,自己缺乏真正的多執行緒思維。本文從自訂鎖 Optex 實作切入,探討並發程式設計的核心心法。

基礎:C# lock 與 Monitor

public class TestThreading {
    private object lockThis = new object();
    public void Function() {
        lock (lockThis) {
            // 存取執行緒敏感資源
        }
    }
}

lock 是語法糖,實際呼叫 Monitor.Enter/Exit。注意 Monitor.Pulse/PulseAll 若等待佇列為空會持續阻塞,可能造成類死鎖。

初版 Optex:語意信號量實作

public sealed class Optex : IDisposable {
    private int m_Waiters = 0;
    private Semaphore m_WaiterLock = new Semaphore(0, Int32.MaxValue);

    public void Enter() {
        Thread.BeginCriticalRegion();
        if (Interlocked.Increment(ref m_Waiters) == 1) return;  // 首個執行緒直接取得
        m_WaiterLock.WaitOne();  // 其餘等待
    }

    public void Exit() {
        if (Interlocked.Decrement(ref m_Waiters) > 0)
            m_WaiterLock.Release(1);  // 喚醒一個等待者
        Thread.EndCriticalRegion();
    }
    ...
}

兩個關鍵認知

  1. Thread.Begin/EndCriticalRegion 非傳統臨界區:僅標示區域內執行緒中止/未處理例外會影響整個 AppDomain,與同步無關。
  2. m_Waiters 優化效能:首個執行緒避免進入核心模式(Semaphore 為核心物件),大幅降低無競爭時的開銷。

優化版 Optex:狀態機與原子操作

public sealed class Optex : IDisposable {
    private int m_LockState = 0;  // 位元組合:0=Free, 1=Owned, 2=WaiterCount
    private const int c_lsFree = 0x0, c_lsOwned = 0x1, c_1Waiter = 0x2;
    private Semaphore m_WaiterLock = new Semaphore(0, Int32.MaxValue);

    public void Enter() {
        Thread.BeginCriticalRegion();
        while (true) {
            int ls = InterlockedOr(ref m_LockState, c_lsOwned);
            if ((ls & c_lsOwned) == c_lsFree) return;  // 成功取得

            if (IfThen(ref m_LockState, ls, ls + c_1Waiter)) {
                m_WaiterLock.WaitOne();
            }
        }
    }

    public void Exit() {
        int ls = InterlockedAnd(ref m_LockState, ~c_lsOwned);
        if (ls != c_lsOwned) {  // 有等待者
            ls &= ~c_lsOwned;
            if (IfThen(ref m_LockState, ls & ~c_lsOwned, ls - c_1Waiter))
                m_WaiterLock.Release(1);
        }
        Thread.EndCriticalRegion();
    }

    // 原子化位元操作:CompareExchange 迴圈
    private static int InterlockedOr(ref int target, int with) { ... }
    private static int InterlockedAnd(ref int target, int with) { ... }
    private static bool IfThen(ref int val, int @if, int then) { ... }
    ...
}

核心思維解析

1. 原子操作是並發基石

InterlockedOr/And 使用 CompareExchange 迴圈確保讀-修改-寫不可分割:

do {
    i = j;
    j = Interlocked.CompareExchange(ref target, i | with, i);
} while (i != j);

若目標值在操作期間被其他執行緒修改,迴圈重試直到成功。這是無鎖程式設計的核心模式。

2. 狀態機設計取代單純計數

單一 int 以位元編碼多重狀態:

  • Bit 0:Owned(是否被持有)
  • Bit 1+:WaiterCount(等待執行緒數)

原子操作同時更新多個邏輯欄位,避免競爭條件。

3. 進入/退出的雙重檢查模式

傳統思維:

Enter: WaitOne()
Exit:  Release(1)

優化思維:

  • Enter:先嘗試原子獲取 → 成功即返回;失敗則登記等待者 → WaitOne
  • Exit:釋放擁有權 → 若有等待者則喚醒一個

關鍵差異:WaitOne/Release 前後皆驗證狀態未被並發修改,確保只在必要時進入核心模式。

4. 處理極端競爭:自旋迴圈

Enter 的 while(true) 解決「搶佔式排程導致獲取後未實際持有」的極罕見情況:

  • 執行緒 A 獲取鎖,尚未執行關鍵區程式碼
  • OS 切換至執行緒 B,B 也嘗試獲取,發現已被持有,登記等待並 WaitOne
  • A 恢復執行,釋放鎖並 Release(1) 喚醒 B
  • B 從 WaitOne 返回,但此時鎖可能已被 C 獲取 → 迴圈重試

這種「虛假喚醒」處理是健壯並發元件的標準做法。

多執行緒思維總結

  1. 假設最壞情況:任何共享資料存取都可能被並發干擾
  2. 最小化核心轉換:用戶模式原子操作優於核心物件
  3. 狀態不可分割:相關變數合併為單一原子字組
  4. 雙重檢查模式:快速路徑(無競爭)+ 慢速路徑(核心同步)
  5. 容忍虛假喚醒:迴圈驗證條件,而非假設喚醒即成功
  6. 可見性與順序:Interlocked 操作隱含 Memory Barrier,確保跨核心可見

結語

多執行緒程式設計複雜但充滿挑戰性。隨著多核心普及,掌握無鎖技術、記憶體模型、鎖優化策略將成為核心競爭力。從 Optex 實作中提煉的思維模式,適用於所有並發控制場景。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved