• 揭示同步块索引(上):从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的代码求助了,对于所有"内部实现"的代码,我们可以在sscli20clrsrcvmecall.cpp里找到映射:

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

    原来Enter映射到JIT_MonEnter,一步步的找过去,我们最终到了这里:

    Sscli20clrsrcvmjithelpers.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背后发生的事情了吧。下一次面试的时候,当别人问你同步块索引的时候,你就可以滔滔不绝的和他论述一番。接下来还有两篇分析同步块的其他作用。

    转自:https://www.cnblogs.com/yuyijq/archive/2009/03/13/1410071.html

  • 相关阅读:
    CMD 下运行python的unittest测试脚本无输出
    Python2 HTMLTestRunner自动化测试报告美化
    Python3 HTMLTestRunner自动化测试报告美化
    Python+Selenium练习篇之5-利用partial link text定位元素
    Python+Selenium练习篇之6-利用class name定位元素
    Python+Selenium练习篇之3-利用tag name定位元素
    Python+Selenium练习篇之4-利用link text定位元素
    Python+Selenium练习篇之2-利用ID定位元素
    Python+Selenium练习篇之1-摘取网页上全部邮箱
    Python+Selenium基础篇之5-第一个完整的自动化测试脚本
  • 原文地址:https://www.cnblogs.com/sheseido/p/11623629.html
Copyright © 2020-2023  润新知