摘要: 本系列意在記錄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
