摘要: 本系列意在記錄Windwos線程的相關知識點,包括線程基礎、線程調度、線程同步、TLS、線程池等。
從這篇開始,在線程同步的方法上,開始在.NET平台上做個總結,同時對比Windows原生的API方法。你可以發現其中的聯系。
.NET中的Monitor和lock
相信很多看官早已對此十分熟悉了。本文作為總結性的文章,有一些篇幅將對比Monitor和關鍵段的關系。由於lock就是Monitor,所以先從Monitor說起,通常Monitor是像下面這樣使用的:
Monitor.Entry(lockObj); try { // lockObj的同步區 } catch(Exception e) { // 異常處理代碼 } finally { Monitor.Exit(lockObj); // 解除鎖定 }
當某個線程在Monitor.Entry返回后就獲得了對其中lockObj的訪問權限,其他試圖獲取lockObj的線程將被阻塞,直到線程調用Monitor.Exit釋放lockObj的所有權。這意味着下面三點:
- 如果lockObj是空閑的,那么第一個調用Entry的線程將立即獲得lockObj;
- 如果調用Entry的線程已經獲准訪問lockObj,那么不會阻塞;
- 如果調用Entry時lockObj已被其他線程鎖定,則線程等待直到lockObj解鎖;
事實上其中的第二點是個重要的特征,這種情況將發生在遞歸的情況下。Monitor應該會記錄線程獲准訪問lockObj的次數,以正確的對鎖定次數進行遞減。
我花了一些時間研究Monitor到底對應底層是什么實現方式,但是我並沒有找到證據證明Monitor和關鍵段有什么必然聯系。但是從表象上看,Monitor的API方式和關鍵段如此相似,而且上述的三個特點也幾乎完全一致,況且MSDN也把Monitor表述成Critical Section,因此,暫且認為Monitor就是關鍵段的包裝吧!
在我之前的文章【Windows】線程漫談——線程同步之關鍵段中詳細介紹了Windows API關鍵段,下面列出這兩種API的對比:
.NET Monitor API | Windows API |
Monitor.Entry(lockObj) | EnterCriticalSection(&cs) |
Monitor.Exit(lockObj) | LeaveCriticalSection(&cs) |
Monitor.TryEntry(lockObj) | TryEnterCriticalSection(&cs) |
-- | InitializeCriticalSection(&cs); |
-- | DeleteCriticalSection(&cs); |
-- | InitializeCriticalSectionAndSpinCount |
-- | SetCriticalSectionSpinCount |
Monitor.Pulse | -- |
Monitor.Wait | -- |
可以看到Monitor簡化了關鍵段的使用,而且還提供了額外的Wait和Pulse方法(因為不常用,因此這里不展開了)。但是如果Monitor真的就是關鍵段實現的話,Monitor卻不能讓我們設置旋轉鎖的嘗試次數,這是一個缺陷。
關於Wait和Pulse順便提一下,我個人認為是條件變量的一個替代方案。關於條件變量詳見【Windows】線程漫談——線程同步之Slim讀/寫鎖。
最后再次強調,這里的對比只是本人一廂情願,未必說Monitor真的就是關鍵段!
針對Monitor鎖定的lockObj有如下問題需要注意:
- lockObj不能是值類型,因為這里會被裝箱,而每次裝箱的引用不同,因此C#在編譯階段就保證了這種限制
- lockObj最好不要是public對象,因為可能會導致死鎖,比如下面這個極端的情況:
public class Foo { public void Bar() { lock (this) { Console.WriteLine("Class:Foo:Method:Bar"); } } } public class MyClient { public void Test() { Foo f = new Foo(); lock (f) //獲准了f對象 { ThreadStart ts = new ThreadStart(f.Bar); Thread t = new Thread(ts); t.Start(); //新線程執行Bar方法需要獲得f的訪問權限,但是已被當前線程鎖定,新線程將阻塞 t.Join(); //新線程將無法返回,死鎖 } } }
- lockObj最好不要是字符串,由於字符串駐留的原因,可能導致死鎖:
public class Foo { public void Bar() { lock ("Const")//Const將駐留 { Console.WriteLine("Class:Foo:Method:Bar"); } } } public class MyClient { private string lockObj = "Const"; public void Test() { Foo f = new Foo(); lock (lockObj) //由於lockObj是"Const","Const"被駐留,所以實際上lock是同一個對象 { ThreadStart ts = new ThreadStart(f.Bar); Thread t = new Thread(ts); t.Start(); //新線程執行Bar方法需要獲得lockObj的訪問權限,但是已被當前線程鎖定,新線程將阻塞 t.Join(); //新線程將無法返回,死鎖 } } }
上面兩個例子已經用了lock而不是Monitor,事實上,lock經過編譯后就是Monitor,但是lock無法使用Monitor.TryEntry:
.try { ... IL_0037: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&) ... } // end .try finally { ... IL_0069: call void [mscorlib]System.Threading.Monitor::Exit(object) ... }
最后,設計一個簡單的帶一個緩沖隊列的Log方法,要求線程安全,下面給出C#的實現(在前面的【Windows】線程漫談——線程同步之關鍵段利用關鍵段給出了C++的實現,這里的代碼結構幾乎一樣,注釋就省略了):
public class LogInfo { public int Level{get;set;} public string Message{get;set;} } public class Log { private static List<LogInfo> LogQueue = new List<LogInfo>(); private static object _lockLog = new object(); private static object _lockQueue = new object(); public void Log(int Level, string Message) { if (Monitor.TryEnter(_lockLog)) { Monitor.Enter(_lockQueue); foreach (var l in LogQueue) { LogInternal(l.Level, l.Message); } LogQueue.Clear(); Monitor.Exit(_lockQueue); LogInternal(Level, Message); Monitor.Exit(_lockLog); } else { Monitor.Enter(_lockQueue); LogQueue.Add(new LogInfo { Level = Level, Message = Message }); Monitor.Exit(_lockQueue); } } protected virtual void LogInternal(int Level, string Message) { //真實的log動作可能會耗費非常長的時間 } }
勞動果實,轉載請注明出處:http://www.cnblogs.com/P_Chou/archive/2012/07/18/monitor-in-net-thread-sync.html