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.FullName, type.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,
основные сущности:
- Статический класс
ThreadPoolGlobals.Содержит глобальное состояние пула потоков для каждого домена. Напомню, что статические члены классов привязаны к доменам даже в случае загрузки их (классов) в Shared Domain. Одним из полей является экземпляр
ThreadPoolWorkQueue. Хочу обратить внимание, что начиная с версии 4.0 CLR поддерживает отдельную очередь рабочих единиц для каждого пользовательского домена и равномерно распределяет время своих потоков между ними. - Класс
ThreadPoolWorkQueue.Умная
очередь рабочих единиц - полностью управляемый код с плюшками типа Work Stealing. - Статический класс
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:
- Извлекает и исполняет элементы из очереди пока не кончился квант времени отведенный пулом:
while ((Environment.TickCount - quantumStartTime) < ThreadPoolGlobals.tpQuantum).Это скорее оптимизация, призванная уменьшить количество переходов между
clr.dllиmscorlib.dll, точнее не тратить больше времени на эти переходы нежели на сами рабочие единицы.Кстати, ТэПэ квантум по-умолчанию равен 30 миллисекундам, а это два кванта операционной системы (в обычных условиях). Наверное это попытка гарантировать минимум один целый квантум для метода.
- После исполнения каждой рабочей единицы пул потоков уведомляется об этом; пул может приказать диспатчеру немедленно выйти из метода:
// // 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; - Каждый раз в случае непустой очереди вызывает
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();
- Первым действием уменьшает счетчик неудовлетворенных запросов с помощью
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
и проанализировать класс более детально.
Во-первых, он предлагает шесть доступных пользовательскому коду методов:
- static ExecutionContext Capture()
- ExecutionContext CreateCopy()
- static bool IsFlowSuppressed()
- static AsyncFlowControl SuppressFlow()
- static void RestoreFlow()
- static void Run(ExecutionContext executionContext, ContextCallback callback, Object state)
Все эти методы описаны в документации к классу; ключевые особенности:
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происходит следующее:SecurityContext.Capture(ecCurrent, ref stackMark)Класс
SecurityContextпо структуре и функциям напоминаетExecutionContext, но работает лишь с безопасностью и ее политиками. Лень углубляться сюда, но ключевые аспекты - Impersonation,WindowsIdentityи CAS Security.HostExecutionContextManager.CaptureHostExecutionContext()Производит захват контекста приложения-хоста текущего домена.
ecCurrent.SynchronizationContext.CreateCopy()При условии соответствующих опций, текущий
SynchronizationContextпотока клонируется.ecCurrent.LogicalCallContext.Clone()Если текущий
LogicalCallContextне пустой - он также клонируется.- Всяческие оптимизации и сборка всего этого в один объект.
- Методы
IsFlowSuppressed,SuppressFlowиRestoreFlowпозволяют запретить захват контекста с помощьюCapture, что внятно описано Рихтером в упомянутой выше книге. - Метод
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.ThreadSystem.Threading.TimerSystem.Windows.Forms.ControlSystem.Windows.Threading.DispatcherOperation- Классы семейства Task Parallel Library
System.IO.StreamSystem.Net.Sockets.SocketSystem.Data.SqlClient.SqlDataReader- И многие другие классы с асинхронными операциями.
У перечисления CaptureOptions есть элемент IgnoreSyncCtx. Он предотвращает захват и клонирование SynchronizationContext
следующими классами:
System.IO.StreamSystem.Threading.Overlapped, который используется многими классами с поддержкой Overlapped IO -Socket,MessageQueue,Pipe,PipeStream,HttpListener,HttpListenerRequestи т.д.System.Threading.Tasks.*System.Threading.ThreadSystem.Threading.ThreadPoolSystem.Threading.TimerSystem.Runtime.CompilerServices.AsyncTaskMethodBuilderSystem.Runtime.CompilerServices.AsyncVoidMethodBuilder
CaptureOptions могут использовать лишь классы самой mscorlib.dll, соответствующие методы объявлены как internal. Пользовательский
код может захватить лишь весь контекст целиком.
Теперь настало время для двух важных граждан ExecutionContext - LogicalCallContext и IllogicalCallContext из пространства
имен (внимание!) System.Runtime.Remoting.Messaging. Сразу напишу, что честных механизмов получить сами эти объекты нет, все спрятано как internal.
Для чтения и модификации этих контейнеров нам с барского плеча подарили класс
CallContext
со следующими операциями (статическими):
void FreeNamedDataSlot(String name)- удаляет элемент из контейнера с соответствующим именем.Object LogicalGetData(String name)- читает значение из текущегоLogicalCallContext.void LogicalSetData(String name, Object data)- записывает туда значение.Object GetData(String name)- делегирует в первую очередь вLogicalGetData, а в случае неудачи читает из текущегоIllogicalCallContext.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(маркер отсутствует).- Пара методов
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(null, null); 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!
Рассмотрим следующий сценарий: окно приложения неактивно, окно Notepad активно. Пользователь производит щелчок мышью по текстовому полю ввода окна приложения. Окно активируется, заголовок меняет свой внешний вид, фокус ввода получает поле ввода. Далее щелчок по окну Notepad. Окно деактивируется. Трассировочный лог происходящего приведен ниже, вместе с детальным разбором всех событий.