Monitor.Wait初探(7)
现在我们再回到最初的示例上来,ThreadProc1和ThreadProc2之间通过lock关键字进行同步,加在在这两个线程上的lock就好比两扇大门,而这两扇门同时只允许打开一扇。我们先在第一个线程中打开了第一扇门,那第二个线程就要在第二扇门外徘徊。而要打开第二扇门就应该等待第一扇门的Monitor.Exit,Exit的调用就好比是关上当前的门,通知另外的门可以打开了。
但是现在似乎出了点”意外“。
但是现在第一扇门打开之后,突然蹦出个Monitor.Wait,这玩意是个人物,它除了让第一个线程处于阻塞状态,还通知第二扇门可以打开了。这也就是说:并不需要等到第一扇门调用Monitor.Exit,第二扇门就可以打开了。
这一切究竟是怎麽发生的?带着种种疑惑,我们慢慢来拨开云雾见青天。
还需要从BOOL SyncBlock::Wait(INT32 timeOut, BOOL exitContext)开头,
该函数在真正的Block当前线程也即是调用isTimedOut = pCurThread->Block(timeOut, &syncState)之前,有一行代码值得研究一番:
syncState.m_EnterCount = LeaveMonitorCompletely();
单看这行代码所调用的函数名称,直译成:彻底离开Monitor,听起来和Monitor.Exit有点异曲同工之妙。
再来看看其实现:
LONG LeaveMonitorCompletely()
{
WRAPPER_CONTRACT;
return m_Monitor.LeaveCompletely();
}
嗯,又调用了
m_Monitor.LeaveCompletely();
这个m_Monitor在SyncBlock类中的定义:
protected:
AwareLock m_Monitor; // the actual monitor
注释说这是实际的Monitor,所以我们应该能猜出这就是Monitor.Enter/Exit所涉及的类(事实上也是如此,因为我很快看到了Monitor.Enter对应的实现就是AwareLock.Enter),是一个AwareLock 的变量。
Ok,我们再来看AwareLock 的LeaveCompletely实现:
LONG AwareLock::LeaveCompletely()
{
WRAPPER_CONTRACT;
LONG count = 0;
while (Leave()) {
count++;
}
_ASSERTE(count > 0); // otherwise we were never in the lock
return count;
}
再看Leave:
BOOL AwareLock::Leave()
{
CONTRACTL
{
INSTANCE_CHECK;
NOTHROW;
GC_NOTRIGGER;
MODE_ANY;
}
CONTRACTL_END;
Thread* pThread = GetThread();
AwareLock::LeaveHelperAction action = LeaveHelper(pThread);
switch(action)
{
case AwareLock::LeaveHelperAction_None:
// We are done
return TRUE;
case AwareLock::LeaveHelperAction_Signal:
// Signal the event
Signal();
return TRUE;
default:
// Must be an error otherwise
_ASSERTE(action == AwareLock::LeaveHelperAction_Error);
return FALSE;
}
}
由此可以看出所谓彻底离开不过就是遍历+Signal();那麽这个Signal函数究竟做了啥,看名字和注释知其一二:Signal the event
void Signal()
{
WRAPPER_CONTRACT;
// CLREvent::SetMonitorEvent works even if the event has not been intialized yet
m_SemEvent.SetMonitorEvent();
}
现在问题又来了,m_SemEvent是啥?首先,定义:
CLREvent m_SemEvent;
是个CLREvent,然后看看其初始化,是在void AwareLock::AllocLockSemEvent()中:
m_SemEvent.CreateMonitorEvent((SIZE_T)this);
啊哈,只看名字就知道这一个Monitor专用的Event,那麽AllocLockSemEvent又被谁调用呢,是BOOL AwareLock::EnterEpilog(Thread* pCurThread, INT32 timeOut),而EnterEpilog又为AwareLock::Enter所调用,事实上当EnterEpilog就是第二扇门的徘回函数。我们来看看怎麽徘徊的:
for (;;)
{
// We might be interrupted during the wait (Thread.Interrupt), so we need an
// exception handler round the call.
EE_TRY_FOR_FINALLY
{
// Measure the time we wait so that, in the case where we wake up
// and fail to acquire the mutex, we can adjust remaining timeout
// accordingly.
start = CLRGetTickCount64();
ret = m_SemEvent.Wait(timeOut, TRUE);
_ASSERTE((ret == WAIT_OBJECT_0) || (ret == WAIT_TIMEOUT));
if (timeOut != (INT32) INFINITE)
{
end = CLRGetTickCount64();
if (end == start)
{
duration = 1;
}
else
{
duration = end - start;
}
duration = min(duration, (DWORD)timeOut);
timeOut -= (INT32)duration;
}
}
要注意关键行
ret = m_SemEvent.Wait(timeOut, TRUE); 下文还会讲到。这明显是在等待事件对象的信号有状态。
再来看看SetMonitorEvent的实现:
void CLREvent::SetMonitorEvent()
{
CONTRACTL
{
NOTHROW;
GC_NOTRIGGER;
}
CONTRACTL_END;// SetMonitorEvent is robust against initialization races. It is possible to
// call CLREvent::SetMonitorEvent on event that has not been initialialized yet by CreateMonitorEvent.
// CreateMonitorEvent will signal the event once it is created if it happens.for (;;)
{
LONG oldFlags = m_dwFlags;if (oldFlags & CLREVENT_FLAGS_MONITOREVENT_ALLOCATED)
{
// Event has been allocated already. Use the regular codepath.
Set();
break;
}LONG newFlags = oldFlags | CLREVENT_FLAGS_MONITOREVENT_SIGNALLED;
if (FastInterlockCompareExchange((LONG*)&m_dwFlags, newFlags, oldFlags) != oldFlags)
{
// We lost the race
continue;
}
break;
}
}又调用了Set函数:
BOOL CLREvent::Set()
{
CONTRACTL
{
NOTHROW;
GC_NOTRIGGER;
PRECONDITION((m_handle != INVALID_HANDLE_VALUE));
}
CONTRACTL_END;_ASSERTE(Thread::AllowCallout());
if (IsOSEvent() || !CLRSyncHosted()) {
return UnsafeSetEvent(m_handle);
}
else {
if (IsAutoEvent()) {
HRESULT hr;
BEGIN_SO_TOLERANT_CODE_CALLING_HOST(GetThread());
hr = ((IHostAutoEvent*)m_handle)->Set();
END_SO_TOLERANT_CODE_CALLING_HOST;
return hr == S_OK;
}
else {
HRESULT hr;
BEGIN_SO_TOLERANT_CODE_CALLING_HOST(GetThread());
hr = ((IHostManualEvent*)m_handle)->Set();
END_SO_TOLERANT_CODE_CALLING_HOST;
return hr == S_OK;
}
}
}
在Set函数中我们看到最终是对m_handle的Set。从而使得事件状态被置成有信号状态,也即释放了所有的lock而使得它们重新处于被调度状态。
现在再回过头来看看AwareLock::EnterEpilog的逻辑,已经知道是通过ret = m_SemEvent.Wait(timeOut, TRUE)等待事件对象的信号状态,而我麽也已经知道在调用Monitor.Wait之后会调用事件对象的Set函数从而使得等待的线程得到锁。那麽为了加深印象,我还想通过Windbg走走。