揭示同步塊索引(上):從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背后發生的事情了吧。下一次面試的時候,當別人問你同步塊索引的時候,你就可以滔滔不絕的和他論述一番。接下來還有兩篇分析同步塊的其他作用。