CodeGym /課程 /C# SELF /診斷的最佳實踐與工具

診斷的最佳實踐與工具

C# SELF
等級 57 , 課堂 4
開放

1. 穩健多執行緒程式碼的祕訣

如果把多執行緒比作一隊同時修車的工人,很容易理解:只要其中一個人去拿別人的工具,維修就卡住了。程式碼也是一樣:對共享資料處理不慎會導致隱藏錯誤,只有在「實戰」時才會出現。

在本講中會說明如何寫出不會像紙牌屋一樣倒的多執行緒程式碼,以及當事情出錯時有哪些工具可以幫忙。

1. 將臨界區(lock)最小化

臨界區內的程式碼越少越好。當一個執行緒持有鎖時,其他執行緒都要等。

範例:


// 不好:整個業務邏輯都在 lock 裡面 — 所有執行緒都在等
lock(_locker)
{
    // 長時間操作(與共享資源無關)
    Thread.Sleep(500);
    counter++;
}

// 好:只在 lock 裡做必要的操作
// 重的工作放在 lock 外面
Thread.Sleep(500);
lock(_locker)
{
    counter++;
}

實務:如果在鎖內呼叫網路或做長時間計算,效能會大幅下降。

2. 不要用通用物件當作鎖的 key

lock(this)lock(typeof(MyClass)) 並不是好主意。

為什麼? 如果別的程式也用同一個物件來鎖,會導致 deadlock 或難以發現的 Bug。應該永遠使用獨立的 private 物件:

private readonly object _locker = new object();

lock(_locker)
{
    // 你的操作
}

禁止:string(string)、public 欄位、值類型物件。

3. 始終用 try...finally 釋放已抓到的資源

任何抓取 Mutex、Semaphore、ReaderWriterLockSlim 的操作,都應該在 finally 中釋放。

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

4. 不要過度同步

只對真正的共享資源(例如集合)進行同步,而不是每個「小動作」都上鎖。過多的鎖會讓程式變成等待隊列。

5. 使用 thread-safe 的集合與類型

.NET 提供了適用於多執行緒場景的專用集合:ConcurrentDictionaryConcurrentQueueConcurrentBagBlockingCollection 等。它們內部已經做好保護。

using System.Collections.Concurrent;

ConcurrentDictionary<int, string> users = new ConcurrentDictionary<int, string>();
users.TryAdd(1, "Vasya");
users[2] = "Petya";

6. 當心 deadlock(互相鎖住)

典型陷阱是以不同順序取得多個鎖。

// 執行緒 1
lock(obj1)
{
    lock(obj2)
    {
        // 做點什麼
    }
}

// 執行緒 2
lock(obj2)
{
    lock(obj1)
    {
        // 做點什麼
    }
}

建議:在所有執行緒中都按相同順序取得鎖。

7. 盡可能使用不可變狀態(immutable state)

如果物件在建立後不會改變狀態,就可以安全地從任何執行緒讀取。例子:stringTupleDateTime,以及只讀的自訂 DTO。

2. 多執行緒問題的診斷工具

同步錯誤很狡猾:發生頻率低而且不可預測。使用能幫你發現與分析它們的工具與方法。

1. 記錄事件與執行緒

記錄當前 Thread.ManagedThreadId 與關鍵操作,是理解「誰在什麼時候」進入/離開臨界區的簡單方式。

Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] 進入臨界區");
// ...
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] 離開臨界區");

對於真實應用,使用 Microsoft.Extensions.LoggingNLogSerilog

2. Thread Sanitizer & Race Detector

在 .NET 裡沒有一個完美內建的 ThreadSanitizer,但有些有用的工具:

3. Visual Studio Diagnostics Tools

Visual Studio 的 profiler 幫你看見:

  • 應用中有哪些執行緒;
  • 哪些執行緒處於等待(waiting);
  • 哪裡有鎖定與競爭;
  • 何時發生 deadlock 與 contention。

收集 trace 以獲得鎖使用的詳細圖譜。

4. Dump 分析與 WinDbg

如果伺服器「掛掉」了,抓取進程 dump 用 WinDbgdotnet-dump 打開。從 stack trace 可以看出執行緒卡在哪、誰持有哪些鎖。

堆疊分析範例:

0:000> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
    1 000001d4b6f90e08          1         1 000001d4b5c941c0 000001d4b6f03458

(通常只有部署經驗豐富的「絕地武士」會去看 dump — 別害怕,這是個強大的工具。)

5. 壓力測試的單元測試(stress testing)

在並行環境下把你的多執行緒程式跑幾百到幾千次迭代——這樣比較容易發現稀有的 race。

[Test]
public void Counter_IsThreadSafe()
{
    var counter = 0;
    var locker = new object();
    var tasks = new List<Task>();
    for (int i = 0; i < 100; i++)
    {
        tasks.Add(Task.Run(() =>
        {
            for (int j = 0; j < 10000; j++)
            {
                lock (locker)
                {
                    counter++;
                }
            }
        }));
    }
    Task.WaitAll(tasks.ToArray());
    Assert.AreEqual(100 * 10000, counter);
}

6. 使用 assert 與專門檢查

在偵錯時加入保證狀態正確的檢查。例如在同一執行緒重複取得資源時用 Debug.Assert

3. 結論與建議

視覺化示意:危險區域與安全

graph TD
    A[共用資源] -- 無同步 --> B(競爭狀態)
    A -- 鎖定 (lock/Mutex) --> C[安全存取:臨界區]
    C -- "過多的鎖" --> D(效能下降)
    A -- ReaderWriterLockSlim --> E{多讀者 / 單寫者}
    E -- "讀取" --> F[多個執行緒可同時讀取]
    E -- "寫入" --> G[只有一個寫,其他等待]

同步原語與用途

原語 用途 允許多少執行緒 跨程序 效能 使用場景
lock (Monitor)
簡單的臨界區 1 非常高 99% 的情況
Mutex
類似的臨界區,但跨進程 1 中等 檔案、IPC
Semaphore
不超過 N 個執行緒 N 中等 資源池
SemaphoreSlim
相同用途,但在進程內更快 N 程式內的池
ReaderWriterLockSlim
多讀者、單寫者 多/1 快取、設定
2
任務
C# SELF, 等級 57, 課堂 4
上鎖
用隨機暫停來解決 Livelock
用隨機暫停來解決 Livelock
1
問卷/小測驗
互鎖阻塞,等級 57,課堂 4
未開放
互鎖阻塞
多執行緒常見問題
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION