zoukankan      html  css  js  c++  java
  • 艾伟_转载:揭示同步块索引(上):从lock开始 狼人:

    大家都知道引用类型对象除实例字段的开销外,还有两个字段的开销:类型指针和同步块索引(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背后发生的事情了吧。下一次面试的时候,当别人问你同步块索引的时候,你就可以滔滔不绝的和他论述一番。接下来还有两篇分析同步块的其他作用。

  • 相关阅读:
    LeetCode 1110. Delete Nodes And Return Forest
    LeetCode 473. Matchsticks to Square
    LeetCode 886. Possible Bipartition
    LeetCode 737. Sentence Similarity II
    LeetCode 734. Sentence Similarity
    LeetCode 491. Increasing Subsequences
    LeetCode 1020. Number of Enclaves
    LeetCode 531. Lonely Pixel I
    LeetCode 1091. Shortest Path in Binary Matrix
    LeetCode 590. N-ary Tree Postorder Traversal
  • 原文地址:https://www.cnblogs.com/waw/p/2157147.html
Copyright © 2011-2022 走看看