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