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

作者: 橫刀天笑  來源: 博客園  發布時間: 2009-08-24 10:06  閱讀: 2496 次  推薦: 0   原文鏈接   [收藏]  

大家都知道引用類型對象除實例字段的開銷外,還有兩個字段的開銷:類型指針和同步塊索引(SyncBlockIndex)。同步塊索引這個東西比起它的兄弟類型指針更少受人關注,顯得有點冷落,其實此兄功力非凡,在CLR里可謂叱咤風云,很多功能都要借助它來實現。 接下來我會用三篇來介紹同步塊索引在.NET中的所作所為。
既然本章副標題是從lock開始,那我就舉幾個lock的示例:

代碼1

   1: public class Singleton
   2: {
   3:     private static object lockHelper = new object();
   4:     private static Singleton _instance = null;
   5:     public static Singleton Instance
   6:     {
   7:         get
   8:         {
   9:             lock(lockHelper)
  10:             {
  11:                 if(_instance == null)
  12:                     _instance = new Singleton();
  13:             }
  14:             return _instance;
  15:         }
  16:     }
  17: } 

代碼2

   1: public class Singleton
   2: {
   3:     private static Singleton _instance = null;
   4:     public static Singleton Instance
   5:     {
   6:         get
   7:         {
   8:             object lockHelper = new object();
   9:             lock(lockHelper)
  10:             {
  11:                 if(_instance==null)
  12:                     _instance = new Singleton();
  13:             }
  14:             return _instance;
  15:         }
  16:     }
  17: } 

代碼3

   1: public class Singleton
   2: {
   3:     private static Singleton _instance = null;
   4:     public static Singleton Instance
   5:     {
   6:         get
   7:         {
   8:             lock(typeof(Singleton))
   9:             {
  10:                 if(_instance==null)
  11:                     _instance = new Singleton();
  12:             }
  13:             return_instance;
  14:         }
  15:     }
  16: } 

代碼4

   1: public void DoSomething()
   2: {
   3:     lock(this)
   4:     {
   5:         //dosomething
   6:     }
   7: } 

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

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

   1: class Singleton
   2: {
   3:     private:
   4:         CRITICAL_SECTIONg_cs;
   5:         static Singleton _instance = NULL;
   6:     public:
   7:         Singleton()
   8:         {
   9:             InitializeCriticalSection(&g_cs);
  10:         }
  11:         static Singleton GetInstance()
  12:         {
  13:             EnterCriticalSection(&g_cs);
  14:             if(_instance!=NULL)
  15:                 _instance=newSingleton();
  16:             LeaveCriticalSection(&g_cs);
  17:             return_instance;
  18:         }
  19:         ~Singleton()
  20:         {
  21:             DeleteCriticalSection(&g_cs);
  22:         }
  23: }

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里定義的:

   1: typedef struct _RTL_CRITICAL_SECTION{
   2: PRTL_CRITICAL_SECTION_DEBUGDebugInfo;
   3: //
   4: //Thefollowingthreefieldscontrolenteringandexitingthecritical
   5: //sectionfortheresource
   6: //
   7: LONG LockCount;
   8: LONG RecursionCount;
   9: HANDLE OwningThread;//fromthethread'sClientId->UniqueThread
  10: HANDLE LockSemaphore;
  11: ULONG _PTRSpinCount;//forcesizeon64-bitsystemswhenpacked
  12: }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的方式,與下面的代碼是等價的:

   1: try{ 
   2:     Monitor.Enter(obj); 
   3:     //… 
   4: }finally{ 
   5:     Monitor.Exit(obj); 
   6: } 

 

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

當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代碼如下所示:

   1: IL_0007:stloc.1
   2: IL_0008:call void [mscorlib]System.Threading.Monitor::Enter(object)
   3: IL_000d:nop
   4: .try
   5: {
   6:     IL_000e:nop
   7:     IL_000f:ldsfld class Singleton Singleton::_instance
   8:     //….
   9:     //…
  10: }
  11: finally
  12: {
  13:     IL_002b:ldloc.1
  14:     IL_002c:call void [mscorlib]System.Threading.Monitor::Exit(object)
  15:     IL_0031:nop
  16:     IL_0032:endfinally
  17: } 

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

   1: [MethodImpl(MethodImplOptions.InternalCall)]
   2: public static extern void Enter(objectobj);
   3: [MethodImpl(MethodImplOptions.InternalCall),ReliabilityContract(Consistency.WillNotCorruptState,Cer.Success)]
   4: public static extern void Exit(objectobj); 

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

   1: FCFuncStart(gMonitorFuncs) 
   2: FCFuncElement("Enter", JIT_MonEnter) 
   3: FCFuncElement("Exit", JIT_MonExit) 
   4:
   5: FCFuncEnd() 

 

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

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

   1: HCIMPL_MONHELPER(JIT_MonEnterWorker_Portable, Object* obj) 
   2: { 
   3:     //省略大部分代碼 
   4:     OBJECTREF objRef = ObjectToOBJECTREF(obj); 
   5:     objRef->EnterObjMonitor(); 
   6: } 
   7: HCIMPLEND 

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

   1: void EnterObjMonitor() 
   2: { 
   3:     GetHeader()->EnterObjMonitor(); 
   4: } 

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

   1: void ObjHeader::EnterObjMonitor() 
   2: { 
   3:     GetSyncBlock()->EnterMonitor(); 
   4: } 

 

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

   1: void EnterMonitor() 
   2: { 
   3:     m_Monitor.Enter(); 
   4: } 

 

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

   1: void AwareLock::Enter() 
   2: { 
   3:     Thread* pCurThread = GetThread(); 
   4:     for (;;) 
   5:     { 
   6:          volatile LONG state = m_MonitorHeld; 
   7:         if (state == 0) 
   8:         { 
   9:             // Common case: lock not held, no waiters. Attempt to acquire lock by 
  10:              // switching lock bit. 
  11:             if (FastInterlockCompareExchange((LONG*)&m_MonitorHeld, 1, 0) == 0) 
  12:             { 
  13:                 break; 
  14:             } 
  15:         } 
  16:         else 
  17:         { 
  18:             // It's possible to get here with waiters but no lock held, but in this 
  19:              // case a signal is about to be fired which will wake up a waiter. So 
  20:              // for fairness sake we should wait too. 
  21:              // Check first for recursive lock attempts on the same thread. 
  22:              if (m_HoldingThread == pCurThread) 
  23:              { 
  24:                  goto Recursion; 
  25:              } 
  26:             // Attempt to increment this count of waiters then goto contention 
  27:             // handling code. 
  28:         if (FastInterlockCompareExchange((LONG*)&m_MonitorHeld, (state + 2), state) == state) 
  29:         { 
  30:              goto MustWait;  
  31:         } 
  32:     } 
  33: } 
  34:     // We get here if we successfully acquired the mutex. 
  35:     m_HoldingThread = pCurThread; 
  36:     m_Recursion = 1; 
  37:     pCurThread->IncLockCount(); 
  38:     return; 
  39: MustWait: 
  40:      // Didn't manage to get the mutex, must wait. 
  41:     EnterEpilog(pCurThread); 
  42:      return; 
  43:     Recursion: 
  44:      // Got the mutex via recursive locking on the same thread. 
  45:     m_Recursion++; 
  46: } 

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

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

總結

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

0
0
 
標簽:CLR
 
 

文章列表

全站熱搜
創作者介紹
創作者 大師兄 的頭像
大師兄

IT工程師數位筆記本

大師兄 發表在 痞客邦 留言(0) 人氣()