В очередной раз стало не хватать свежачка из внутренностей .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!