大家都知道引用類型對象除實例字段的開銷外,還有兩個字段的開銷:類型指針和同步塊索引 (SyncBlockIndex)。同步塊索引這個東西比起它的兄弟類型指針更少受人關注,顯得有點冷落,其實此 兄功力非凡,在CLR裡可謂叱咤風雲,很多功能都要借助它來實現。
接下來我會用三篇來介紹同步塊索引在.NET中的所作所為。
既然本章副標題是從lock開始,那我就舉幾個lock的示例:
代碼1
public class Singleton
{
private static object lockHelper = new object();
private static Singleton _instance = null;
public static Singleton Instance
{
get
{
lock (lockHelper)
{
if (_instance == null)
_instance = new Singleton();
}
return _instance;
}
}
}
代碼2
public class Singleton
{
private static Singleton _instance = null;
public static Singleton Instance
{
get
{
object lockHelper = new object();
lock (lockHelper)
{
if (_instance == null)
_instance = new Singleton();
}
return _instance;
}
}
}
代碼3
public class Singleton
{
private static Singleton _instance = null;
public static Singleton Instance
{
get
{
lock (typeof(Singleton))
{
if (_instance == null)
_instance = new Singleton();
}
return _instance;
}
}
}
代碼4
public void DoSomething()
{
lock (this)
{
//do something
}
}
上面四種代碼,對於加鎖的方式來說(不討論其他)哪一種是上上選?對於這個問題的答案留在本文 最後解答。
讓我們先來看看在Win32的時代,我們如何做到CLR中的lock的效果。在Win32時,Windows為我們提供 了一個CRITICAL_SECTION結構,看看上面的單件模式,如果使用CRITICAL_SECTION的方式如何實現?
class Singleton
{
private:
CRITICAL_SECTION g_cs;
static Singleton _instance = NULL;
public:
Singleton()
{
InitializeCriticalSection(&g_cs);
}
static Singleton GetInstance()
{
EnterCriticalSection(&g_cs);
if(_instance != NULL)
_instance = new Singleton();
LeaveCriticalSection(&g_cs);
return _instance;
}
~Singleton()
{
DeleteCriticalSection(&g_cs);
}
}
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裡定義的:
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
//
// The following three fields control entering and exiting the critical
// section for the resource
//
LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread; // from the thread's ClientId->UniqueThread
HANDLE LockSemaphore;
ULONG_PTR SpinCount; // force size on 64-bit systems when packed
} 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的方式,與下面的代碼是等價的:
try{
Monitor.Enter(obj);
//…
}finally{
Monitor.Exit(obj);
}
(以下內容只限制在本文,為了簡單,有的說法很片面,更詳細的內容會在後面兩篇裡描述)
當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代碼如下所示:
IL_0007: stloc.1
IL_0008: call void [mscorlib]System.Threading.Monitor::Enter(object)
IL_000d: nop
.try
{
IL_000e: nop
IL_000f: ldsfld class Singleton Singleton::_instance
//….
//…
finally
{
IL_002b: ldloc.1
IL_002c: call void [mscorlib]System.Threading.Monitor::Exit(object)
IL_0031: nop
IL_0032: endfinally
}
為了簡單,我省去了一部分代碼。但是很明顯,我們看到了System.Threading.Monitor.Enter和Exit 。然後我們拿出Reflector看看這個Monitor到底是何方神聖。哎呀,發現Monitor.Enter和Monitor.Exit 的代碼如下所示:
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void Enter(object obj);
[MethodImpl(MethodImplOptions.InternalCall), ReliabilityContract (Consistency.WillNotCorruptState, Cer.Success)]
public static extern void Exit(object obj);
只見方法使用了extern關鍵字,方法上面還標有[MethodImpl(MethodImplOptions.InternalCall)]這 樣的特性,實際上這說明Enter和Exit的代碼是在內部C++的代碼實現的。只好拿出Rotor的代碼求助了, 對於所有"內部實現"的代碼,我們可以在sscli20\clr\src\vm\ecall.cpp裡找到映射:
FCFuncStart(gMonitorFuncs)
FCFuncElement("Enter", JIT_MonEnter)
FCFuncElement("Exit", JIT_MonExit)
…
FCFuncEnd()
原來Enter映射到JIT_MonEnter,一步步的找過去,我們最終到了這裡:
Sscli20\clr\src\vm\jithelpers.cpp:
HCIMPL_MONHELPER (JIT_MonEnterWorker_Portable, Object* obj)
{
//省略大部分代碼
OBJECTREF objRef = ObjectToOBJECTREF(obj);
objRef->EnterObjMonitor();
}
HCIMPLEND
objRef就是object的引用,EnterObjMonitor方法的代碼如下:
void EnterObjMonitor()
{
GetHeader()- >EnterObjMonitor();
}
GetHeader()方法獲取對象頭ObjHeader,在ObjHeader裡有對EnterObjMonitor()方法的定義:
void ObjHeader::EnterObjMonitor()
{
GetSyncBlock()- >EnterMonitor();
}
GetSyncBlock()方法會獲取該對象對應的SyncBlock,在SyncBlock裡有EnterMonitor方法的定義:
void EnterMonitor()
{
m_Monitor.Enter ();
}
離核心越來越近了,m_Monitor是一個AwareLock類型的字段,看看AwareLock類內Enter方法的定義:
void AwareLock::Enter()
{
Thread *pCurThread = GetThread();
for (;;)
{
volatile LONG state = m_MonitorHeld;
if (state == 0)
{
// Common case: lock not held, no waiters. Attempt to acquire lock by
// switching lock bit.
if (FastInterlockCompareExchange((LONG*) &m_MonitorHeld, 1, 0) == 0)
{
break;
}
}
else
{
// It's possible to get here with waiters but no lock held, but in this
// case a signal is about to be fired which will wake up a waiter. So
// for fairness sake we should wait too.
// Check first for recursive lock attempts on the same thread.
if (m_HoldingThread == pCurThread)
{
goto Recursion;
}
// Attempt to increment this count of waiters then goto contention
// handling code.
if (FastInterlockCompareExchange((LONG*) &m_MonitorHeld, (state + 2), state) == state)
{
goto MustWait;
}
}
}
// We get here if we successfully acquired the mutex.
m_HoldingThread = pCurThread;
m_Recursion = 1;
pCurThread->IncLockCount();
return;
MustWait:
// Didn't manage to get the mutex, must wait.
EnterEpilog(pCurThread);
return;
Recursion:
// Got the mutex via recursive locking on the same thread.
m_Recursion++;
}
從上面的代碼我們可以看到,先使用GetThread()獲取當前的線程,然後取出m_MonitorHeld字段,如 果現在沒有線程進入臨界區,則設置該字段的狀態,然後將m_HoldingThread設置為當前線程,從這一點 上來這與Win32的過程應該是一樣的。如果從m_MonitorHeld字段看,有線程已經進入臨界區則分兩種情況 :第一,是否已進入的線程如當前線程是同一個線程,如果是,則把m_Recursion遞加,如果不是,則通 過EnterEpilog(pCurThread)方法,當前線程進入線程等待隊列。
通過上面的文字描述和代碼的跟蹤,在我們的大腦中應該有這樣一張圖了:
總結
現在你應該知道lock背後發生的事情了吧。下一次面試的時候,當別人問你同步塊索引的時候,你就 可以滔滔不絕的和他論述一番。接下來還有兩篇分析同步塊的其他作用。
歡迎拍磚,祝編程愉快。