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