揭示同步块索引(上):从lock开始 约定不等于承诺〃 2021-12-23 23:27 166阅读 0赞 转自:http://www.cnblogs.com/yuyijq/archive/2009/03/13/1410071.html 大家都知道引用类型对象除实例字段的开销外,还有两个字段的开销:类型指针和同步块索引(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)方法,当前线程进入线程等待队列。 通过上面的文字描述和代码的跟踪,在我们的大脑中应该有这样一张图了: ![031209_2321_loc1.gif][] 转载于:https://www.cnblogs.com/jgsbwcx/p/8980607.html [031209_2321_loc1.gif]: /images/20211223/0bfff03009c14de99238e84f5e9997b7.png
相关 Java中的同步代码块和Lock接口对比 在Java中,同步代码块和Lock接口都是用来实现线程间的互斥访问,以防止数据竞争和竞态条件。 1. 同步代码块: - 是一种简洁的同步方式,使用`synchroniz 谁借莪1个温暖的怀抱¢/ 2024年09月18日 23:57/ 0 赞/ 30 阅读
相关 Java多线程同步问题:实例揭示 在Java中,多线程的同步问题主要体现在共享资源的访问控制上。这里我们通过一个具体的实例来揭示这些问题。 假设我们有两个线程A和B,它们需要共同访问一个变量count,但 c 冷不防/ 2024年09月11日 23:21/ 0 赞/ 34 阅读
相关 同步方法和同步块 > 从上一节中可以看到Java中多线程是不安全的,比如多人同时买票,票数会出现负数的情况;多人取钱,钱数会出现负数;多个线程操作同一个列表会出现多个线程覆盖列表统一地方的错误等 缺乏、安全感/ 2023年01月18日 09:25/ 0 赞/ 191 阅读
相关 环形队列(索引从0开始(C语言、C++、Java)、索引从1开始(Matlab)) 环形队列 综合分析 0索引开始的代码实现(C++) 从1索引开始的代码实现(Matlab) 综合分析 关于数组模拟环形队列的问题,有两点疑问。 超、凢脫俗/ 2022年12月01日 15:36/ 0 赞/ 187 阅读
相关 Python_从零开始-同步学习进度 目录 1.0-Python初识 在编程界,有一句话非常出名,叫“Talk is cheap, show me the code.”——空谈无益,秀代码 print( 野性酷女/ 2022年11月05日 05:28/ 0 赞/ 205 阅读
相关 开始学前端,从行元素和块元素区别开始(第一篇) 行元素:不会独占一行,宽度随着内容的变化而变化,对其设置宽度和高度无效,外边距(margin)和padding(内边距)的垂直方向无效,水平方向有效。只能嵌套行内元素。 块元 ╰+攻爆jí腚メ/ 2022年08月21日 00:23/ 0 赞/ 142 阅读
相关 Java并发学习之十四——使用Lock同步代码块 本文是学习网络上的文章时的总结,感谢大家无私的分享。 Java提供另外的机制用来同步代码块。它比synchronized关键字更加强大、灵活。Lock 接口比synchron 淡淡的烟草味﹌/ 2022年08月12日 01:59/ 0 赞/ 172 阅读
相关 7. Lock 同步锁 下载 安装 MySQL 8.0 1-下载 MySQL 1.1-官方下载 1.2-百度云下载 2-安装 MySQL 2.1 迷南。/ 2022年08月02日 09:40/ 0 赞/ 226 阅读
相关 揭示同步块索引(上):从lock开始 转自:http://www.cnblogs.com/yuyijq/archive/2009/03/13/1410071.html 大家都知道引用类型对象除实例字段的开销外,还 约定不等于承诺〃/ 2021年12月23日 23:27/ 0 赞/ 167 阅读
还没有评论,来说两句吧...