程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 揭示同步塊索引(上):從lock開始

揭示同步塊索引(上):從lock開始

編輯:關於.NET

大家都知道引用類型對象除實例字段的開銷外,還有兩個字段的開銷:類型指針和同步塊索引 (SyncBlockIndex)。同步塊索引這個東西比起它的兄弟類型指針更少受人關注,顯得有點冷落,其實此 兄功力非凡,在CLR裡可謂叱咤風雲,很多功能都要借助它來實現。

接下來我會用三篇來介紹同步塊索引在.NET中的所作所為。

既然本章副標題是從lock開始,那我就舉幾個lock的示例:

代碼1

public class Singleton
{
    private static object lockHelper = new object();

    private static Singleton _instance = null;

    public static Singleton Instance
    {
        get
        {
            lock (lockHelper)
            {
                if (_instance == null)
                    _instance = new Singleton();
            }

            return _instance;
        }
    }
}

代碼2

public class Singleton
{
    private static Singleton _instance = null;

    public static Singleton Instance
    {
        get
        {
            object lockHelper = new object();

            lock (lockHelper)
            {
                if (_instance == null)
                    _instance = new Singleton();
            }

            return _instance;
        }
    }
}

代碼3

public class Singleton
{
    private static Singleton _instance = null;

    public static Singleton Instance
    {
        get
        {
            lock (typeof(Singleton))
            {
                if (_instance == null)
                    _instance = new Singleton();
            }

            return _instance;
        }
    }
}

代碼4

public void DoSomething()
    {
        lock (this)
        {
            //do something
        }
}

上面四種代碼,對於加鎖的方式來說(不討論其他)哪一種是上上選?對於這個問題的答案留在本文 最後解答。

讓我們先來看看在Win32的時代,我們如何做到CLR中的lock的效果。在Win32時,Windows為我們提供 了一個CRITICAL_SECTION結構,看看上面的單件模式,如果使用CRITICAL_SECTION的方式如何實現?

class Singleton
    {
        private:
            CRITICAL_SECTION g_cs;
            static Singleton _instance = NULL;
        public:
            Singleton()
            {
                InitializeCriticalSection(&g_cs);
            }
            static Singleton GetInstance()
            {
                EnterCriticalSection(&g_cs);
                if(_instance != NULL)
                    _instance = new Singleton();
                LeaveCriticalSection(&g_cs);
                return _instance;
            }
            ~Singleton()
            {
                DeleteCriticalSection(&g_cs);
            }
    }

Windows提供四個方法來操作這個CRITICAL_SECTION,在構造函數裡我們使用 InitializeCriticalSection這個方法初始化這個結構,它知道如何初始化CRITICAL_SECTION結構的成員 ,當我們要進入一個臨界區訪問共享資源時,我們使用EnterCriticalSection方法,該方法首先會檢查 CRITICAL_SECTION的成員,檢查是否已經有線程進入了臨界區,如果有,則線程會等待,否則會設置 CRITICAL_SECTION的成員,標識出本線程進入了臨界區。當臨界區操作結束後,我們使用 LeaveCriticalSection方法標識線程離開臨界區。在Singleton類的析構函數裡,我們使用 DeleteCriticalSection方法銷毀這個結構。整個過程就是如此。

我們可以在WinBase.h裡找到CRITICAL_SECTION的定義:

typedef RTL_CRITICAL_SECTION CRITICAL_SECTION;

可以看到,CRITICAL_SECTION實際上就是RTL_CRITICAL_SECTION,而RTL_CRITICAL_SECTION又是在 WinNT.h裡定義的:

typedef struct _RTL_CRITICAL_SECTION {
    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;

    //
    //  The following three fields control entering and exiting the critical
    //  section for the resource
    //

    LONG LockCount;
    LONG RecursionCount;
    HANDLE OwningThread;        // from the thread's ClientId->UniqueThread
    HANDLE LockSemaphore;
    ULONG_PTR SpinCount;        // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

從上面的定義和注釋,聰明的你肯定知道Windows API提供的這幾個方法是如何操作CRITICAL_SECTION 結構的吧。在這裡我們只需要關注OwningThread成員,當有線程進入臨界區的時候,這個成員就會指向當 前線程的句柄。

說了這麼多,也許有人已經厭煩了,不是說好說lock麼,怎麼說半天Win32 API呢,實際上CLR的lock 與Win32 API實現方式幾乎是一樣的。但CLR並沒有提供CRITICAL_SECTION結構,不過CLR提供了同步塊, CLR還提供了System.Threading.Monitor類。

實際上使用lock的方式,與下面的代碼是等價的:

try{

 Monitor.Enter(obj);

//…

}finally{

  Monitor.Exit(obj);

}

(以下內容只限制在本文,為了簡單,有的說法很片面,更詳細的內容會在後面兩篇裡描述)

當CLR初始化的時候,CLR會初始化一個SyncBlock的數組,當一個線程到達Monitor.Enter方法時,該 線程會檢查該方法接受的參數的同步塊索引,默認情況下對象的同步塊索引是一個負數,那麼表明該對象 並沒有一個關聯的同步塊,CLR就會在全局的SyncBlock數組裡找到一個空閒的項,然後將數組的索引賦值 給該對象的同步塊索引,SyncBlock的內容和CRITICAL_SECTION的內容很相似,當Monitor.Enter執行時, 它會設置SyncBlock裡的內容,標識出已經有一個線程占用了,當另外一個線程進入時,它就會檢查 SyncBlock的內容,發現已經有一個線程占用了,該線程就會等待,當Monitor.Exit執行時,占用的線程 就會釋放SyncBlock,其他的線程可以進入操作了。

好了,有了上面的解釋,我們現在可以判斷本文前面給出的幾個代碼,哪一個是上上選呢?

對於代碼2,鎖定的對象是作為一個局部變量,每個線程進入的時候,鎖定的對象都會不一樣,它的 SyncBlock每一次都是重新分配的,這個根本談不上什麼鎖定不鎖定。

對於代碼3,一般說來應該沒有什麼事情,但這個操作卻是很危險的,typeof(Singleton)得到的是 Singleton的Type對象,所有Singleton實例的Type都是同一個,Type對象也是一個對象,它也有自己的 SyncBlock,Singleton的Type對象的SyncBlock在程序中只會有一份,為什麼說這種做法是危險的呢?如 果在該程序中,其他毫不相干的地方我們也使用了lock(typeof(Singleton)),雖然它和這裡的鎖定毫無 關系,但是只要一個地方鎖定了,各個地方的線程都會在等待。

對於代碼4,實際上代碼4的性質和代碼3差不多,如果有一個地方使用了DoSomething方法所在類的實 例進行lock,而且恰好如this是同一個實例,那麼兩個地方就會互斥了。

由此看來只有代碼1是上上選,之所以是這樣,是因為代碼1將鎖定的對象作為私有字段,只有這個對 象內部可以訪問,外部無法鎖定。

上面只是從文字上敘說,也許你覺得證據不足,我們就搬來代碼作證。

使用ILDasm反編譯上面單件模式的Instance屬性的代碼,其中一段IL代碼如下所示:

IL_0007:  stloc.1
  IL_0008:  call       void [mscorlib]System.Threading.Monitor::Enter(object)
  IL_000d:  nop
  .try
  {
    IL_000e:  nop
    IL_000f:  ldsfld     class Singleton Singleton::_instance
//….
//…
finally
  {
    IL_002b:  ldloc.1
    IL_002c:  call       void [mscorlib]System.Threading.Monitor::Exit(object)
    IL_0031:  nop
    IL_0032:  endfinally
  }

為了簡單,我省去了一部分代碼。但是很明顯,我們看到了System.Threading.Monitor.Enter和Exit 。然後我們拿出Reflector看看這個Monitor到底是何方神聖。哎呀,發現Monitor.Enter和Monitor.Exit 的代碼如下所示:

[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void Enter(object obj);
[MethodImpl(MethodImplOptions.InternalCall), ReliabilityContract (Consistency.WillNotCorruptState, Cer.Success)]
public static extern void Exit(object obj);

只見方法使用了extern關鍵字,方法上面還標有[MethodImpl(MethodImplOptions.InternalCall)]這 樣的特性,實際上這說明Enter和Exit的代碼是在內部C++的代碼實現的。只好拿出Rotor的代碼求助了, 對於所有"內部實現"的代碼,我們可以在sscli20\clr\src\vm\ecall.cpp裡找到映射:

FCFuncStart(gMonitorFuncs)

    FCFuncElement("Enter", JIT_MonEnter)

    FCFuncElement("Exit", JIT_MonExit)

   …

FCFuncEnd()

原來Enter映射到JIT_MonEnter,一步步的找過去,我們最終到了這裡:

Sscli20\clr\src\vm\jithelpers.cpp:

HCIMPL_MONHELPER (JIT_MonEnterWorker_Portable, Object* obj)

{

//省略大部分代碼

OBJECTREF objRef = ObjectToOBJECTREF(obj);

     objRef->EnterObjMonitor();

}

HCIMPLEND

objRef就是object的引用,EnterObjMonitor方法的代碼如下:

    void EnterObjMonitor()

{

        GetHeader()- >EnterObjMonitor();

}

GetHeader()方法獲取對象頭ObjHeader,在ObjHeader裡有對EnterObjMonitor()方法的定義:

void ObjHeader::EnterObjMonitor()

{

    GetSyncBlock()- >EnterMonitor();

}

GetSyncBlock()方法會獲取該對象對應的SyncBlock,在SyncBlock裡有EnterMonitor方法的定義:

    void EnterMonitor()

    {

        m_Monitor.Enter ();

}

離核心越來越近了,m_Monitor是一個AwareLock類型的字段,看看AwareLock類內Enter方法的定義:

void AwareLock::Enter()

{

Thread *pCurThread = GetThread();

     for (;;)

{

    volatile LONG state = m_MonitorHeld;

         if (state == 0)

        {

            // Common case: lock not held, no waiters. Attempt to acquire lock by

            // switching lock bit.

            if (FastInterlockCompareExchange((LONG*) &m_MonitorHeld, 1, 0) == 0)

            {

                 break;

            }

        }

         else

        {

            // It's possible to get here with waiters but no lock held, but in this

            // case a signal is about to be fired which will wake up a waiter. So

            // for fairness sake we should wait too.

            // Check first for recursive lock attempts on the same thread.

            if (m_HoldingThread == pCurThread)

            {   

                goto Recursion;

            }

            // Attempt to increment this count of waiters then goto contention

            // handling code.

            if (FastInterlockCompareExchange((LONG*) &m_MonitorHeld, (state + 2), state) == state)

            {

                 goto MustWait;

            }

         }

    }

    // We get here if we successfully acquired the mutex.

    m_HoldingThread = pCurThread;

    m_Recursion = 1;

     pCurThread->IncLockCount();

    return;

MustWait:

    // Didn't manage to get the mutex, must wait.

    EnterEpilog(pCurThread);

     return;

Recursion:

    // Got the mutex via recursive locking on the same thread.

    m_Recursion++;

}

從上面的代碼我們可以看到,先使用GetThread()獲取當前的線程,然後取出m_MonitorHeld字段,如 果現在沒有線程進入臨界區,則設置該字段的狀態,然後將m_HoldingThread設置為當前線程,從這一點 上來這與Win32的過程應該是一樣的。如果從m_MonitorHeld字段看,有線程已經進入臨界區則分兩種情況 :第一,是否已進入的線程如當前線程是同一個線程,如果是,則把m_Recursion遞加,如果不是,則通 過EnterEpilog(pCurThread)方法,當前線程進入線程等待隊列。

通過上面的文字描述和代碼的跟蹤,在我們的大腦中應該有這樣一張圖了:

總結

現在你應該知道lock背後發生的事情了吧。下一次面試的時候,當別人問你同步塊索引的時候,你就 可以滔滔不絕的和他論述一番。接下來還有兩篇分析同步塊的其他作用。

歡迎拍磚,祝編程愉快。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved