在 C# 多線程開發中,lock 關鍵字是最常見的同步手段。然而,許多開發者可能不知道,lock 實際上只是編譯器提供的語法糖,其底層實際調用的是 System.Threading.Monitor 類。理解 Monitor 的工作原理,不僅能幫助我們更靈活地控制線程同步,還能掌握避免死鎖、實現超時機制等進階技巧。
觀察以下簡單的同步代碼:
public static void MyLock()
{
lock (typeof(Program))
{
}
}
編譯為 IL 後,會發現 lock 被轉換為 Monitor.Enter 和 Monitor.Exit 的調用。這意味著 lock 只是 Monitor 的一層封裝,並在結束時自動調用 Exit 釋放鎖,類似於 using 語句的資源自動釋放機制。
若直接使用 Monitor 類,必須顯式調用 Exit,並配合 try-finally 確保鎖在任何情況下(異常、返回、正常執行)都能被釋放:
Monitor.Enter(lockObj);
try
{
// 同步代碼區
}
catch (Exception e)
{
// 異常處理
}
finally
{
Monitor.Exit(lockObj); // 確保解鎖
}
Monitor 提供了 TryEnter 方法,允許指定等待超時時間(毫秒)。這對於避免死鎖至關重要:
if (Monitor.TryEnter(lockObj, 1000)) // 等待 1 秒
{
try
{
// 臨界區操作
}
finally
{
Monitor.Exit(lockObj);
}
}
else
{
// 超時處理邏輯
}
若超時時間設為 Timeout.Infinite,則等同於 Enter;設為 0 則立即返回不等待。
經典的死鎖場景是兩個線程按相反順序請求兩把鎖。使用 TryEnter 並設置超時,可使線程在無法獲取鎖時主動退出,從而打破死鎖條件:
class Program
{
private static Object objA = new Object();
private static Object objB = new Object();
public static void LockA()
{
if (Monitor.TryEnter(objA, 1000))
{
Thread.Sleep(1000);
if (Monitor.TryEnter(objB, 2000))
{
Monitor.Exit(objB);
}
else
{
Console.WriteLine("LockB timeout");
}
Monitor.Exit(objA);
}
Console.WriteLine("LockA");
}
public static void LockB()
{
if (Monitor.TryEnter(objB, 2000))
{
Thread.Sleep(2000);
if (Monitor.TryEnter(objA, 1000))
{
Monitor.Exit(objA);
}
else
{
Console.WriteLine("LockA timeout");
}
Monitor.Exit(objB);
}
Console.WriteLine("LockB");
}
public static void Main()
{
Thread threadA = new Thread(LockA);
Thread threadB = new Thread(LockB);
threadA.Start();
threadB.Start();
Thread.Sleep(4000);
Console.WriteLine("線程結束");
}
}
運行結果顯示,兩個線程均能在超時後繼續執行,避免了永久阻塞。
Monitor 類不僅是 lock 的底層實現,更提供了 TryEnter、Wait、Pulse 等豐富同步原語。掌握其用法,能讓我們在複雜併發場景下實現更精細、更安全的線程控制。
