【Windows】線程漫談——.NET線程同步之Monitor和lock


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM