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走走。

原文地址:https://www.cnblogs.com/dancewithautomation/p/2416641.html