Hungry Mind , Blog about everything in IT - C#, Java, C++, .NET, Windows, WinAPI, ...

Xbox 360 Wireless Controller for Windows

Купил себе Xbox 360 Wireless Controller for Windows, для ПК. Некоторые игры засияли во всей красе, например заброшенный почти сразу Prince of Persia Forgotten Sands. Невероятно удобный манипулятор! Втыкнул ресивер в USB, засунул батарейки в устройство, включил - все, можно играть.

Xbox 360 Wireless Controller for Windows

Visual C++ concurrency runtime bug

#include <thread>
#include <mutex>
#include <condition_variable>
 
std::mutex m1;
std::mutex m2;
 
std::condition_variable e1;
std::condition_variable e2;
 
void t1()
{   
   for (;;) {
      {
      std::lock_guard<std::mutex> lg(m1);
      e1.notify_one();
      }
      std::unique_lock<std::mutex> m2Lock(m2);
      e2.wait_for(m2Lock, std::chrono::milliseconds(15));
   }
}
 
void t2()
{   
   for (;;) {
      {
      std::lock_guard<std::mutex> lg(m2);
      e2.notify_one();
      }
      std::unique_lock<std::mutex> m1Lock(m1);
      e1.wait_for(m1Lock, std::chrono::milliseconds(15));
   }
}
 
int _tmain(int argc, _TCHAR* argv[])
{
   const auto hc = std::thread::hardware_concurrency();
   std::thread thread1(&t1);
   std::thread thread2(&t2);
   thread1.join();
   thread2.join();
   return 0;
}
 

Вот такая простая программа падает со следующим сообщением:

Unhandled exception at 0x0000000077A32782 (ntdll.dll) in Win32ConInv.exe: 0xC000000D: An invalid parameter was passed to a service or function.

Stack:

 	ntdll.dll!string "Enabling heap debug options\n"()	Unknown
>	msvcr110d.dll!__crtWaitForThreadpoolTimerCallbacks(_TP_TIMER * pti, int fCancelPendingCallbacks) Line 539	C
 	msvcr110d.dll!Concurrency::details::DeleteAsyncTimerAndUnloadLibrary(_TP_TIMER * timer) Line 696	C++
 	msvcr110d.dll!Concurrency::details::TimedSingleWaitBlock::destroyTimer(bool waitForOutstandingCallback) Line 457	C++
 	msvcr110d.dll!Concurrency::details::TimedSingleWaitBlock::Satisfy(Concurrency::Context * * pContextOut, Concurrency::details::EventWaitNode * pNode) Line 484	C++
 	msvcr110d.dll!Concurrency::details::EventWaitNode::Satisfy(Concurrency::Context * * pContextOut) Line 330	C++
 	msvcr110d.dll!Concurrency::details::_Condition_variable::notify_one() Line 645	C++
 	msvcp110d.dll!do_signal(_Cnd_internal_imp_t * * cond, int all) Line 68	C++
 	msvcp110d.dll!_Cnd_signal(_Cnd_internal_imp_t * * cond) Line 84	C++
 	Win32ConInv.exe!std::_Cnd_signalX(_Cnd_internal_imp_t * * _Cnd) Line 108	C++
 	Win32ConInv.exe!std::condition_variable::notify_one() Line 51	C++
 	Win32ConInv.exe!t1() Line 21	C++
 	Win32ConInv.exe!std::_Bind<1,void,void (__cdecl*const)(void),std::_Nil,std::_Nil,std::_Nil,std::_Nil,std::_Nil,std::_Nil,std::_Nil>::operator()() Line 1152	C++
 	Win32ConInv.exe!std::_LaunchPad >::_Run(std::_LaunchPad > * _Ln) Line 196	C++
 	Win32ConInv.exe!std::_LaunchPad >::_Go() Line 188	C++
 	msvcp110d.dll!_Call_func(void * _Data) Line 52	C++
 	msvcr110d.dll!_callthreadstartex() Line 354	C
 	msvcr110d.dll!_threadstartex(void * ptd) Line 337	C
 	kernel32.dll!BaseThreadInitThunk()	Unknown
 	ntdll.dll!RtlUserThreadStart()	Unknown

А вот и дефект на Microsoft Connect со статусом Closed. Stephan T. Lavavej обещает the fix will be available in the next release of our C++ Standard Library implementation, 12/17/2012. Дефект открыт 9/13/2012, сейчас 04/28/2013. Пиздец!

MethodDescriptor from _methodPtrAux

.prefer_dml 1
r $t0 = 7fe95b5c088 + 5; .printf /D "<link cmd=\"!DumpMD %p\">DumpMD</link>", poi($t0 + 8*by($t0+2) + 3) + 8*by($t0+1)

CLR Execution Context

В очередной раз стало не хватать свежачка из внутренностей .NET Framework и CLR - решил поизучать потоки и домены, а точнее контекст, в котором выполняется наш прекрасный код.

Начнем с примера:

public class ContextInspector : MarshalByRefObject
{
    public void DumpContext(Object state)
    {
        var domain = AppDomain.CurrentDomain;
        var domainName = domain.IsDefaultAppDomain() ? "Default Domain" : domain.FriendlyName;
        Console.WriteLine("#" + Thread.CurrentThread.ManagedThreadId + ": Domain = " + domainName);
    }
 
    public void QueueDumpContext()
    {
        ThreadPool.QueueUserWorkItem(DumpContext);
    }
}
 
public class Program1
{
    private static void Main(string[] args)
    {
        new ContextInspector().QueueDumpContext();
        Thread.Yield();
        var domain = AppDomain.CreateDomain("Custom Domain");
        var type = typeof(ContextInspector);
        var contextInspector = (ContextInspector)domain.CreateInstanceAndUnwrap(type.Assembly.FullNametype.FullName);
        // We just need the same thread pool thread at least once
        contextInspector.QueueDumpContext();
        contextInspector.QueueDumpContext();
        Console.ReadLine();
    }
}

Программа помещает вызов метода DumpContext в очередь пула потоков CLR из основного домена приложения (Default Domain), а после из специализированного (Custom Domain), созданного мною. Точный вывод программы не очень интересен, но следующие две строки порождают важные вопросы:

#6: Domain = Default Domain
#6: Domain = Custom Domain

Почему один и тот же рабочий поток находится в разных доменах? Чем мотивировано такое поведение и кто его обеспечивает?

Вполне логично ожидать выполнения рабочих единиц (Work Items) пула потоков в окружении, которое максимально напоминает исходное. Фактически, единственное различие должно быть в потоке выполнения и всем, что с этим прямо или косвенно связано. И здесь CLR вместе с BCL делают нам, разработчикам, большое одолжение - запоминают наиболее важные аспекты контекста выполнения и разворачивают (применяют) их в потоках пула перед выполнением пользовательских рабочих единиц. Необходимо напомнить, что классы mscorlib.dll (в т.ч. ThreadPool) загружаются единожды в т.н. Shared Domain и поэтому они (вместе с состоянием, результатом JIT компиляции IL кода и пр.) используются всеми классами всех без исключения доменов. Подробнее об этом можно почитать здесь.

Пул потоков CLR реализован как связка управляемого кода в лице класса ThreadPool и неуправляемой составляющей внутри библиотеки clr.dll, классы ThreadpoolMgr и ThreadPoolNative. Давайте рассмотрим первую часть с доступным исходным кодом из Reference Source, файл ThreadPool.cs, основные сущности:

  1. Статический класс ThreadPoolGlobals.

    Содержит глобальное состояние пула потоков для каждого домена. Напомню, что статические члены классов привязаны к доменам даже в случае загрузки их (классов) в Shared Domain. Одним из полей является экземпляр ThreadPoolWorkQueue. Хочу обратить внимание, что начиная с версии 4.0 CLR поддерживает отдельную очередь рабочих единиц для каждого пользовательского домена и равномерно распределяет время своих потоков между ними.

  2. Класс ThreadPoolWorkQueue.

    Умная очередь рабочих единиц - полностью управляемый код с плюшками типа Work Stealing.

  3. Статический класс ThreadPool

    Интерфейс взаимодействия между пользовательским кодом и всей инфраструктутой пула потоков CLR. Подавляющее большинство методов объявлены как static extern и реализованы в clr.dll.

Разберем процесс попадания рабочей единицы в пул и ее диспатчеризацию. Все начинается с метода QueueUserWorkItem, за которым следует добавление элемента в очередь и вызов RequestWorkerThread:

[InlinedCallFrame: 00000000004ec728] System.Threading.ThreadPool.RequestWorkerThread()
DomainNeutralILStubClass.IL_STUB_PInvoke()
System.Threading.ThreadPoolWorkQueue.Enqueue(System.Threading.IThreadPoolWorkItem, Boolean)
System.Threading.ThreadPool.QueueUserWorkItemHelper(System.Threading.WaitCallback, System.Object, System.Threading.StackCrawlMark ByRef, Boolean)
System.Threading.ThreadPool.QueueUserWorkItemHelper(System.Threading.WaitCallback, System.Object, System.Threading.StackCrawlMark ByRef, Boolean)
System.Threading.ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback, System.Object)

Метод RequestWorkerThread объявлен как static extern, его реализация находится вот здесь:

bp clr!ThreadPoolNative::RequestWorkerThread

И до безобразия проста:

call    clr!GetThread
mov     rcx,qword ptr [rax+10h]
...
call    clr!ThreadpoolMgr::SetAppDomainRequestsActive
...
call    clr!ThreadpoolMgr::QueueUserWorkItem
...

Текущий домен маркируется как требующий диспатчеризации из очереди, а сбивающий с толку вызов ThreadpoolMgr::QueueUserWorkItem на самом деле взаимодействует с приложением-хостом через clr!CorHost2::m_HostThreadpoolManager.

Теперь проанализируем как пул обрабатывает запрос на рабочий поток. Каждый из них начинает свою жизнь следующим образом:

clr!ThreadpoolMgr::WorkerThreadStart
clr!Thread::intermediateThreadProc+0x7d
KERNEL32!BaseThreadInitThunk+0xd
ntdll!RtlUserThreadStart+0x1d

clr!ThreadpoolMgr::WorkerThreadStart производит некую инициализацию:

...
call    clr!ClrFlsSetThreadType
...
call    qword ptr [clr!_imp_CoInitializeEx]
...

Далее вызов clr!ThreadpoolMgr::ExecuteWorkRequest выполняет следующий код по стеку:

clr!Frame::Push+0x90
clr!Frame::Pop+0x86
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x2bd
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x23b
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0xb4

Инструкция clr!Frame::Push+0x90 - call clr!ManagedThreadCallState::IsAppDomainEqual проверяет текущий домен на соответствие необходимому:

0:005> g
Breakpoint 1 hit
clr!Frame::Push+0x95:
000007fe`f38035d1 85c0            test    eax,eax
0:005> r eax
eax=0

и изменяет его в случае неравенства вызовом clr!AppDomain::EnterContext:

clr!AppDomain::EnterContext
clr!Thread::DoADCallBack+0x21d
clr!Frame::Push+0xd6
clr!Frame::Pop+0x86
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x2bd
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x23b
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0xb4
clr!ThreadpoolMgr::ExecuteWorkRequest+0x4c
clr!ThreadpoolMgr::WorkerThreadStart+0xf3
clr!Thread::intermediateThreadProc+0x7d
KERNEL32!BaseThreadInitThunk+0xd
ntdll!RtlUserThreadStart+0x1d

который просто прыгает в другую функцию:

jmp     clr!Thread::EnterContextRestricted

И в упрощенной форме смена домена осуществляется записью его в Thread Local Storage:

mov     ecx,dword ptr [clr!gAppDomainTLSIndex]
call    qword ptr [clr!_imp_TlsSetValue]

Далее мы возвращаемся к clr!Frame::Push+0x90 и еще раз проверяем текущий домен:

clr!Frame::Push+0x90
clr!Frame::Pop+0x86
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x2bd
clr!ReturnToPreviousAppDomainHolder::Init+0x39
clr!Thread::DoADCallBack+0x234

На этот раз имеет место равенство и мы попадаем в clr!QueueUserWorkItemManagedCallback, где происходят следующие вызовы:

mscorlib_ni+0x49c790
clr!CallDescrWorkerInternal+0x83
clr!CallDescrWorkerWithHandler+0x4a
clr!MethodDescCallSite::CallTargetWorker+0x2e6
clr!QueueUserWorkItemManagedCallback+0x2a

mscorlib_ni+0x49c790 - это на самом деле метод PerformWaitCallback класса _ThreadPoolWaitCallback:

// 
// This type is necessary because VS 2010's debugger looks for a method named _ThreadPoolWaitCallbacck.PerformWaitCallback 
// on the stack to determine if a thread is a ThreadPool thread or not.  We have a better way to do this for .NET 4.5, but
// still need to maintain compatibility with VS 2010.  When compat with VS 2010 is no longer an issue, this type may be 
// removed.
//
internal static class _ThreadPoolWaitCallback
{ 
    [System.Security.SecurityCritical]
    static internal bool PerformWaitCallback() 
    { 
        return ThreadPoolWorkQueue.Dispatch();
    } 
}

Как вы видите - один из потоков пула был выделен для диспатчеризации рабочих единиц одного из пользовательских доменов.

Рассмотрим ключевые моменты метода Dispatch:

  1. Извлекает и исполняет элементы из очереди пока не кончился квант времени отведенный пулом: while ((Environment.TickCount - quantumStartTime) < ThreadPoolGlobals.tpQuantum).

    Это скорее оптимизация, призванная уменьшить количество переходов между clr.dll и mscorlib.dll, точнее не тратить больше времени на эти переходы нежели на сами рабочие единицы.

    Кстати, ТэПэ квантум по-умолчанию равен 30 миллисекундам, а это два кванта операционной системы (в обычных условиях). Наверное это попытка гарантировать минимум один целый квантум для метода.

  2. После исполнения каждой рабочей единицы пул потоков уведомляется об этом; пул может приказать диспатчеру немедленно выйти из метода:
    //
    // Notify the VM that we executed this workitem.  This is also our opportunity to ask whether Hill Climbing wants 
    // us to return the thread to the pool or not.
    // 
    if (!ThreadPool.NotifyWorkItemComplete()) 
        return false;
    
  3. Каждый раз в случае непустой очереди вызывает workQueue.EnsureThreadRequested(), тем самым запрашивая дополнительные потоки у пула (но следит чтобы количество неудовлетворенных запросов было всегда меньше количества процессоров в системе):
    //
    // If we found work, there may be more work.  Ask for another thread so that the other work can be processed 
    // in parallel.  Note that this will only ask for a max of #procs threads, so it's safe to call it for every dequeue.
    //
    workQueue.EnsureThreadRequested();
       
  4. Первым действием уменьшает счетчик неудовлетворенных запросов с помощью workQueue.MarkThreadRequestSatisfied():
    //
    // Update our records to indicate that an outstanding request for a thread has now been fulfilled. 
    // From this point on, we are responsible for requesting another thread if we stop working for any 
    // reason, and we believe there might still be work in the queue.
    // 
    // Note that if this thread is aborted before we get a chance to request another one, the VM will
    // record a thread request on our behalf.  So we don't need to worry about getting aborted right here.
    //
    workQueue.MarkThreadRequestSatisfied(); 
       

Резюме:

  • ThreadPool управляет очередью задач своего домена, запрашивает ресурсы у пула потоков и за отведенное время исполняет выполняет максимальное количество единиц из очереди.
  • CLR управляет пулом потоков и распределяет процессорное время между очередями всех существующих доменов, которые инициализировали ThreadPool (фактически минимум один раз поместили задачу в очередь). Кроме этого, CLR накатывает правильный низкоуровневый контекст прежде чем передавать управление в класс ThreadPool.

Кстати, clr!ThreadpoolMgr::WorkerThreadStart в цикле обработки запросов передает управление clr!Thread::SetApartment, что приводит к последовательным вызовам CoUninitialize и CoInitializeEx(NULL, COINIT_MULTITHREADED). А значит состояние ole32.dll постоянно очищается и все потоки пула живут в MTA.

Следующий шаг - более тщательное изучение класса ThreadPool, начну с метода QueueUserWorkItemHelper (кстати, обратите внимание на коментарии к коду, они подтверждают часть написанного выше):

//ThreadPool has per-appdomain managed queue of work-items. The VM is
//responsible for just scheduling threads into appdomains. After that 
//work-items are dispatched from the managed queue. 
[System.Security.SecurityCritical]  // auto-generated
private static bool QueueUserWorkItemHelper(WaitCallback callBack, Object state, ref StackCrawlMark stackMark, bool compressStack ) 
{
    bool success =  true;

    if (callBack != null) 
    {
                //The thread pool maintains a per-appdomain managed work queue. 
        //New thread pool entries are added in the managed queue. 
        //The VM is responsible for the actual growing/shrinking of
        //threads. 

        EnsureVMInitialized();

        // 
        // If we are able to create the workitem, we need to get it in the queue without being interrupted
        // by a ThreadAbortException. 
        // 
        try { }
        finally 
        {
            QueueUserWorkItemCallback tpcallBack = new QueueUserWorkItemCallback(callBack, state, compressStack, ref stackMark);
            ThreadPoolGlobals.workQueue.Enqueue(tpcallBack, true);
            success = true; 
        }
    } 
    else 
    {
        throw new ArgumentNullException("WaitCallback"); 
    }
    return success;
}

Экземпляры QueueUserWorkItemCallback добавляются в очередь и на этом финита. Но интерес здесь вызывает уже конструктор QueueUserWorkItemCallback:

internal QueueUserWorkItemCallback(WaitCallback waitCallback, Object stateObj, bool compressStack, ref StackCrawlMark stackMark) 
{
    callback = waitCallback; 
    state = stateObj; 
    if (compressStack && !ExecutionContext.IsFlowSuppressed())
    { 
        // clone the exection context
        context = ExecutionContext.Capture(
            ref stackMark,
            ExecutionContext.CaptureOptions.IgnoreSyncCtx | ExecutionContext.CaptureOptions.OptimizeDefaultCase); 
    }
} 

Обратите внимание на использование параметров compressStack и stackMark вместе с классом ExecutionContext. Без дальнейшего углубления читатель может сообразить что здесь происходит - захват текущего контекста выполнения вместе с неким анализом стека в переменную-член QueueUserWorkItemCallback. Возможности ExecutionContext мимолетно описаны Рихтером в известной книге CLR via C#, а нам самое время переместиться в файл ExecutionContext.cs и проанализировать класс более детально.

Во-первых, он предлагает шесть доступных пользовательскому коду методов:

  1. static ExecutionContext Capture()
  2. ExecutionContext CreateCopy()
  3. static bool IsFlowSuppressed()
  4. static AsyncFlowControl SuppressFlow()
  5. static void RestoreFlow()
  6. static void Run(ExecutionContext executionContext, ContextCallback callback, Object state)

Все эти методы описаны в документации к классу; ключевые особенности:

  1. Capture захватывает текущий контекст в экземпляр своего же класса.

    Здесь стоит остановиться для изучения тела метода (MSDN сокрушает наше мировозрение одним предложением Captures the execution context from the current thread - прям захватывает и пиздец, без компромиссов):

    public static ExecutionContext Capture()
    { 
        // set up a stack mark for finding the caller
        StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; 
        return ExecutionContext.Capture(ref stackMark, CaptureOptions.None); 
    }
    
    static internal ExecutionContext Capture(ref StackCrawlMark stackMark, CaptureOptions options)
    { 
        ExecutionContext.Reader ecCurrent = Thread.CurrentThread.GetExecutionContextReader();
    
        // check to see if Flow is suppressed 
        if (ecCurrent.IsFlowSuppressed)
            return null; 
    
        //
        // Attempt to capture context.  There may be nothing to capture...
        // 
    
    #if FEATURE_IMPERSONATION || FEATURE_COMPRESSEDSTACK 
        // capture the security context 
        SecurityContext secCtxNew = SecurityContext.Capture(ecCurrent, ref stackMark);
    #endif // #if FEATURE_IMPERSONATION || FEATURE_COMPRESSEDSTACK 
    #if FEATURE_CAS_POLICY
         // capture the host execution context
        HostExecutionContext hostCtxNew = HostExecutionContextManager.CaptureHostExecutionContext();      
    #endif // FEATURE_CAS_POLICY 
    
    #if FEATURE_SYNCHRONIZATIONCONTEXT 
        SynchronizationContext syncCtxNew = null; 
    #endif
        LogicalCallContext logCtxNew = null; 
    
        if (!ecCurrent.IsNull)
        {
    #if FEATURE_SYNCHRONIZATIONCONTEXT 
            // capture the [....] context
            if (0 == (options & CaptureOptions.IgnoreSyncCtx)) 
                syncCtxNew = (ecCurrent.SynchronizationContext == null) ? null : ecCurrent.SynchronizationContext.CreateCopy(); 
    #endif // #if FEATURE_SYNCHRONIZATIONCONTEXT
    
            // copy over the Logical Call Context
            if (ecCurrent.LogicalCallContext.HasInfo)
                logCtxNew = ecCurrent.LogicalCallContext.Clone();
        } 
    
        // 
        // If we didn't get anything but defaults, and we're allowed to return the 
        // dummy default EC, don't bother allocating a new context.
        // 
        if (0 != (options & CaptureOptions.OptimizeDefaultCase) &&
    #if FEATURE_IMPERSONATION || FEATURE_COMPRESSEDSTACK
            secCtxNew == null &&
    #endif 
    #if FEATURE_CAS_POLICY
            hostCtxNew == null && 
    #endif // FEATURE_CAS_POLICY 
    #if FEATURE_SYNCHRONIZATIONCONTEXT
            syncCtxNew == null && 
    #endif // #if FEATURE_SYNCHRONIZATIONCONTEXT
            (logCtxNew == null || !logCtxNew.HasInfo))
        {
            return s_dummyDefaultEC; 
        }
    
        // 
        // Allocate the new context, and fill it in.
        // 
        ExecutionContext ecNew = new ExecutionContext();
    #if FEATURE_IMPERSONATION || FEATURE_COMPRESSEDSTACK
        ecNew.SecurityContext = secCtxNew;
        if (ecNew.SecurityContext != null) 
            ecNew.SecurityContext.ExecutionContext = ecNew;
    #endif 
    #if FEATURE_CAS_POLICY 
        ecNew._hostExecutionContext = hostCtxNew;
    #endif // FEATURE_CAS_POLICY 
    #if FEATURE_SYNCHRONIZATIONCONTEXT
        ecNew._syncContext = syncCtxNew;
    #endif // #if FEATURE_SYNCHRONIZATIONCONTEXT
        ecNew.LogicalCallContext = logCtxNew; 
        ecNew.isNewCapture = true;
    
        return ecNew; 
    }
    

    Без оглядки на все эти #if FEATURE_XYZ происходит следующее:

    1. SecurityContext.Capture(ecCurrent, ref stackMark)

      Класс SecurityContext по структуре и функциям напоминает ExecutionContext, но работает лишь с безопасностью и ее политиками. Лень углубляться сюда, но ключевые аспекты - Impersonation, WindowsIdentity и CAS Security.

    2. HostExecutionContextManager.CaptureHostExecutionContext()

      Производит захват контекста приложения-хоста текущего домена.

    3. ecCurrent.SynchronizationContext.CreateCopy()

      При условии соответствующих опций, текущий SynchronizationContext потока клонируется.

    4. ecCurrent.LogicalCallContext.Clone()

      Если текущий LogicalCallContext не пустой - он также клонируется.

    5. Всяческие оптимизации и сборка всего этого в один объект.
  2. Методы IsFlowSuppressed, SuppressFlow и RestoreFlow позволяют запретить захват контекста с помощью Capture, что внятно описано Рихтером в упомянутой выше книге.
  3. Метод Run, естественно, применяет переданный контекст на текущий поток, исполняет код делегата и в конце восстанавливает контекст до исходного состояния. Применяет - фактически прописывает его в поле m_ExecutionContext класса Thread (да-да, именно там хранится вся эта байда).

Класс ThreadPool содержит еще один метод для помещения рабочей задачи в очередь - UnsafeQueueUserWorkItem. Взглянем на пару похожих методов:

[System.Security.SecuritySafeCritical]  // auto-generated 
[MethodImplAttribute(MethodImplOptions.NoInlining)] // Methods containing StackCrawlMark local var has to be marked non-inlineable
public static bool QueueUserWorkItem(
     WaitCallback           callBack,     // NOTE: we do not expose options that allow the callback to be queued as an APC
     Object                 state 
     )
{ 
    StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; 
    return QueueUserWorkItemHelper(callBack,state,ref stackMark,true);
} 

[System.Security.SecurityCritical]  // auto-generated_required
[MethodImplAttribute(MethodImplOptions.NoInlining)] // Methods containing StackCrawlMark local var has to be marked non-inlineable 
public static bool UnsafeQueueUserWorkItem(
     WaitCallback           callBack,     // NOTE: we do not expose options that allow the callback to be queued as an APC 
     Object                 state 
     )
{ 
    StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
    return QueueUserWorkItemHelper(callBack,state,ref stackMark,false);
}

Разница в поведении - параметр compressStack, он влияет на захват и развертывание контекста. Метод UnsafeQueueUserWorkItem не оперирует контекстом - не сохраняет его как часть рабочего элемента очереди, не восстанавливает перед исполнением задачи. Также отключить перенос контекста можно выполнив ThreadPool.SuppressFlow(). Но здесь есть небольшой нюанс - оба метода отмечены как [SecurityCritical], что требует Full Trust у вызывающего метода. Безопасность здесь на высоте - код без Full Trust не может использовать пул для выполнения задач вне собственного контекста. Те же ограничения касаются других классов, например SecurityContext.

Разработчик обязан знать о наличии пар таких методов и разнице между ними. ThreadPool не так прост как кажется, затраты его обслуживания могут оказаться выше затрат исполнения элементов очереди.

Где еще используется ExecutionContext?

  • System.Threading.Thread
  • System.Threading.Timer
  • System.Windows.Forms.Control
  • System.Windows.Threading.DispatcherOperation
  • Классы семейства Task Parallel Library
  • System.IO.Stream
  • System.Net.Sockets.Socket
  • System.Data.SqlClient.SqlDataReader
  • И многие другие классы с асинхронными операциями.

У перечисления CaptureOptions есть элемент IgnoreSyncCtx. Он предотвращает захват и клонирование SynchronizationContext следующими классами:

  • System.IO.Stream
  • System.Threading.Overlapped, который используется многими классами с поддержкой Overlapped IO - Socket, MessageQueue, Pipe, PipeStream, HttpListener, HttpListenerRequest и т.д.
  • System.Threading.Tasks.*
  • System.Threading.Thread
  • System.Threading.ThreadPool
  • System.Threading.Timer
  • System.Runtime.CompilerServices.AsyncTaskMethodBuilder
  • System.Runtime.CompilerServices.AsyncVoidMethodBuilder

CaptureOptions могут использовать лишь классы самой mscorlib.dll, соответствующие методы объявлены как internal. Пользовательский код может захватить лишь весь контекст целиком.

Теперь настало время для двух важных граждан ExecutionContext - LogicalCallContext и IllogicalCallContext из пространства имен (внимание!) System.Runtime.Remoting.Messaging. Сразу напишу, что честных механизмов получить сами эти объекты нет, все спрятано как internal. Для чтения и модификации этих контейнеров нам с барского плеча подарили класс CallContext со следующими операциями (статическими):

  1. void FreeNamedDataSlot(String name) - удаляет элемент из контейнера с соответствующим именем.
  2. Object LogicalGetData(String name) - читает значение из текущего LogicalCallContext.
  3. void LogicalSetData(String name, Object data) - записывает туда значение.
  4. Object GetData(String name) - делегирует в первую очередь в LogicalGetData, а в случае неудачи читает из текущего IllogicalCallContext.
  5. void SetData(String name, Object data) - здесь немного интересней:
    [System.Security.SecurityCritical]  // auto-generated 
    public static void SetData(String name, Object data)
    { 
        if (data is ILogicalThreadAffinative) 
        {
            LogicalSetData(name, data); 
        }
        else
        {
            ExecutionContext ec = Thread.CurrentThread.GetMutableExecutionContext(); 
            ec.LogicalCallContext.FreeNamedDataSlot(name);
            ec.IllogicalCallContext.SetData(name, data); 
        } 
    }
    

    Значение проверяется на интерфейс-маркер ILogicalThreadAffinative, после вызов делегируется в LogicalCallContext (маркер присутствует) или IllogicalCallContext (маркер отсутствует).

  6. Пара методов Header[] GetHeaders() иvoid SetHeaders(Header[] headers) - оперируют заголовками типа Header, внеполосные данные, которые исполняемая среда присоединяет к запросу на вызов метода из другого контекста.

Теперь рассмотрим классы, методы и всеразличные манипуляции, связанные с этими контейнерами:

  • Пара методов BeginInvoke/EndInvoke у классов-делегатов.

    Да-да, нубасики, эти методы не так просты какими они кажутся. Во-первых, код Invoke, BeginInvoke и EndInvoke порождает JIT компилятор во время выполнения (в сборках эти методы отмечены атрибутом [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)]):

    0:000> !DumpMT -md 000007fef1c680c0
    EEClass:         000007fef15c45d8
    Module:          000007fef15c1000
    Name:            System.Action
    mdToken:         0000000002000015
    File:            C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    BaseSize:        0x40
    ComponentSize:   0x0
    Slots in VTable: 16
    Number of IFaces in IFaceMap: 2
    --------------------------------------
    MethodDesc Table
               Entry       MethodDesc    JIT Name
    000007fef1a2c480 000007fef15ca9d0 PreJIT System.Object.ToString()
    000007fef1ab75e0 000007fef16a1898 PreJIT System.MulticastDelegate.Equals(System.Object)
    000007fef1a36c40 000007fef16a1908 PreJIT System.MulticastDelegate.GetHashCode()
    000007fef1aad790 000007fef15caa18 PreJIT System.Object.Finalize()
    000007fef1abeb70 000007fef16a7c38 PreJIT System.Delegate.DynamicInvokeImpl(System.Object[])
    000007fef1b24010 000007fef16a18e0 PreJIT System.MulticastDelegate.GetInvocationList()
    000007fef19ba2c8 000007fef16a1918 PreJIT System.MulticastDelegate.GetMethodImpl()
    000007fef1ab70d0 000007fef16a18d0 PreJIT System.MulticastDelegate.CombineImpl(System.Delegate)
    000007fef1ab7450 000007fef16a18d8 PreJIT System.MulticastDelegate.RemoveImpl(System.Delegate)
    000007fef2287eb0 000007fef16a7ce0 PreJIT System.Delegate.Clone()
    000007fef22886e0 000007fef16a1890 PreJIT System.MulticastDelegate.GetObjectData(System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext)
    000007fef1a2eb30 000007fef16a1910 PreJIT System.MulticastDelegate.GetTarget()
    000007fef19f08c8 000007fef16ad2d0   NONE System.Action.Invoke()
    000007fef19f08a0 000007fef16ad2e8   NONE System.Action.BeginInvoke(System.AsyncCallback, System.Object)
    000007fef19f08b0 000007fef16ad300   NONE System.Action.EndInvoke(System.IAsyncResult)
    000007fef19f08c0 000007fef16ad2b8   NONE System.Action..ctor(System.Object, IntPtr)
    

    В результате, например, код

    Action action = () => Thread.Yield();
    var asyncResult = action.BeginInvoke(nullnull);
    action.EndInvoke(asyncResult);
    return;

    приводит к следующему стеку вызовов:

    (MethodDesc 000007fef15cff58 +0x21 System.Threading.ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback, System.Object)), calling (MethodDesc 000007fef15d01c0 +0 System.Threading.ThreadPool.QueueUserWorkItemHelper(System.Threading.WaitCallback, System.Object, System.Threading.StackCrawlMark ByRef, Boolean))
    (MethodDesc 000007fef1796e78 +0x305 System.Runtime.Remoting.Proxies.RemotingProxy.Invoke(System.Object, System.Runtime.Remoting.Proxies.MessageData ByRef)), calling (MethodDesc 000007fef15cff58 +0 System.Threading.ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback, System.Object))
    clr!CTPMethodTable__CallTargetHelper3+0x12
    clr!MemberLoader::FM_GetStrCompFunc+0x8b, calling clr!CTPMethodTable__CallTargetHelper3
    clr!MethodDesc::IsCtor+0x12, calling clr!MethodDesc::GetAttrs
    clr!CTPMethodTable::OnCall+0x1ce, calling clr!MemberLoader::FM_GetStrCompFunc+0x1c
    clr!CTPMethodTable::PreCall+0x6a, calling clr!TPMethodFrame::GetSlotNumber
    clr!TransparentProxyStub_CrossContextPatchLabel+0xa, calling clr!CTPMethodTable::OnCall
    clr!ThePreStub+0x5a, calling clr!PreStubWorker
    (MethodDesc 000007fe93493968 +0x75 ConsoleApplication.Program1.Main(System.String[])), calling 000007fef19f08a0 (stub for System.Action.BeginInvoke(System.AsyncCallback, System.Object))
    

    Или в упрощенной форме:

    System.Threading.ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback, System.Object)
    System.Runtime.Remoting.Proxies.RemotingProxy.Invoke(System.Object, System.Runtime.Remoting.Proxies.MessageData ByRef)
    [TPMethodFrame: 00000000005febb8] System.Action.BeginInvoke(System.AsyncCallback, System.Object)
    ConsoleApplication.Program1.Main(System.String[]) [d:\Development\Projects\CLRExecutionContext\ConsoleApplication\Program.cs @ 52]
    

    Обратите внимание на обилие всяких ТэПэ в стеках. Открыв файл remotingproxy.cs, можно обнаружить следующий метод:

    // Invoke for case where call is in the same context as the server object
    // (This special static method is used for AsyncDelegate-s ... this is called 
    // directly from the EE) 
    private static void Invoke(Object NotUsed, ref MessageData msgData)
    

    И такой незамысловатый его код:

    case Message.BeginAsync:
    case Message.BeginAsync | Message.OneWay: 
        // pick up call context from the thread 
        m.Properties[Message.CallContextKey] =
            Thread.CurrentThread.GetMutableExecutionContext().LogicalCallContext.Clone();
        ar = new AsyncResult(m);
        AgileAsyncWorkerItem  workItem =
            new AgileAsyncWorkerItem(
                    m, 
                    ((callType & Message.OneWay) != 0) ?
                        null : ar, d.Target); 
    
        ThreadPool.QueueUserWorkItem(
            new WaitCallback( 
                    AgileAsyncWorkerItem.ThreadPoolCallBack),
            workItem);
    
        if ((callType & Message.OneWay) != 0) 
        {
            ar.SyncProcessMessage(null); 
        } 
        m.PropagateOutParameters(null, ar);
        break; 
    case (Message.EndAsync | Message.OneWay):
        return;
    
    case Message.EndAsync: 
        // This will also merge back the call context
        // onto the thread that called EndAsync 
        RealProxy.EndInvokeHelper(m, false);
        break;
    

    AgileAsyncWorkerItem находится в том же файле:

    internal class AgileAsyncWorkerItem 
    {
        private IMethodCallMessage _message; 
        private AsyncResult        _ar;
        private Object             _target;
    
        [System.Security.SecurityCritical]  // auto-generated 
        public AgileAsyncWorkerItem(IMethodCallMessage message, AsyncResult ar, Object target)
        { 
            _message = new MethodCall(message); 
            _ar = ar;
            _target = target; 
        }
    
        [System.Security.SecurityCritical]  // auto-generated
        public static void ThreadPoolCallBack(Object o) 
        {
            ((AgileAsyncWorkerItem) o).DoAsyncCall(); 
        } 
    
    
        [System.Security.SecurityCritical]  // auto-generated
        public void DoAsyncCall()
        {
            (new StackBuilderSink(_target)).AsyncProcessMessage(_message, _ar); 
        }
    }
    

    Как видите, для выполнения вызова используется класс StackBuilderSink, метод AsyncProcessMessage, который применяет LogicalCallContext на поток выполнения:

    LogicalCallContext callCtx =  (LogicalCallContext) 
        mcMsg.Properties[Message.CallContextKey];
    
    ...
    
    // install call context onto the thread, holding onto 
    // the one that is currently on the thread 
    
    oldCallCtx = CallContext.SetLogicalCallContext(callCtx); 
    isCallContextSet = true;
    

    Обратите внимение как метод void Invoke(Object NotUsed, ref MessageData msgData) возвращает экземпляр IAsyncResult с помощью m.PropagateOutParameters(null, ar); он объявлен в файле RealProxy.cs:

    [MethodImplAttribute(MethodImplOptions.InternalCall)]
    public extern void PropagateOutParameters(Object[] OutArgs, Object retVal); 
    

    EndInvoke приводит к вызову RealProxy.EndInvokeHelper(m, false), который выполняет слияние текущего контекста и удаленного:

    // Merge the call context back into the thread that 
    // called EndInvoke
    Thread.CurrentThread.GetMutableExecutionContext().LogicalCallContext.Merge( 
        mrm.LogicalCallContext); 
    

    Из-за всего этого производительность предложенной APM модели для делегатов может, мягко говоря, не порадовать любителей многопоточного программирования. Уверен, польза от LogicalCallContext в одном случае из миллиона, а страдают все. Браво, Microsoft! Особенно порадовала эта бредятина в стеке вместе с PropagateOutParameters.

  • Семейство классов пространства ммен System.Runtime.Remoting (в котором и объявлен LogicalCallContext), естественно, используют его в любых удаленных операциях, поддерживаемых подсистемой Remoting. Вспоминаем ContextBoundObject, __TransparentProxy, всевозможные XyzSink и пр.

На этом пока все. До новых встреч, мой любимый бред bread!

Visual Studio 2012 Update 2 Now Available

Copyright 2007-2011 Chabster