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.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
со следующими операциями (статическими):
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!
3 коммент.:
I am sure this paragraph has touched all the internet visitors,
its really really nice paragraph on building up new website.
hokey
hokey
hokey
hokey
hokey
http://2009.earthfreq.com/forumtopic/fast_options_cheap_twitter_followers_for_free_and_experience
http://173-254-66-136.unifiedlayer.com/learn-about-how-produce-your-shopping-list-laser-targeted-twitter-followers.
html
http://209-20-69-40.slicehost.net/node/6861
http://193.254.231.9/forum/profile.php?mode=viewprofile&u=1624407
http://163.20.113.133/principle/profile.php?mode=viewprofile&u=123
Here is my homepage - hokey
Truly when someone doesn't understand afterward its up to other viewers that they will assist, so here it takes place.
http://abc.djc.com.tw/userinfo.php?uid=7747
http://50-87-72-80.unifiedlayer.com/blogs/270029/284822/very-simple-remedies-inside-of-h
http://acmcustomdesign.com/story.php?id=1188139
http://520hero.com/content/imagination-very-easy-insurance-policies-buy-twitter-followers-cheap
http://acsocial.com/index.php?do=/profile-1322/info/
Look into my web-site sukan
https://github.com/dotnet/coreclr/blob/master/src/vm/win32threadpool.cpp source code
Отправить комментарий