Полные и рабочие методы расширения для получения SynchronizationContext
от Thread
или ExecutionContext
(или null
, если его нет) или DispatcherSynchronizationContext
от Dispatcher
.Протестировано на .NET 4.6.2 .
using Ectx = ExecutionContext;
using Sctx = SynchronizationContext;
using Dctx = DispatcherSynchronizationContext;
public static class _ext
{
// DispatcherSynchronizationContext from Dispatcher
public static Dctx GetSyncCtx(this Dispatcher d) => d?.Thread.GetSyncCtx() as Dctx;
// SynchronizationContext from Thread
public static Sctx GetSyncCtx(this Thread th) => th?.ExecutionContext?.GetSyncCtx();
// SynchronizationContext from ExecutionContext
public static Sctx GetSyncCtx(this Ectx x) => __get(x);
/* ... continued below ... */
}
Все вышеперечисленные функции в итоге вызывают код __get
, показанный ниже, что требует некоторого объяснения.
Обратите внимание, что __get
является статическим полем, предварительно инициализированным отбрасываемым лямбда-блоком.Это позволяет нам аккуратно перехватить первого вызывающего абонента only , чтобы выполнить однократную инициализацию, которая готовит крошечный и постоянный заменяющий делегат, который намного быстрее и не требует отражения.
Последним действием для инициализации intrepid является замена замены на '__get', что одновременно и трагически означает, что код сбрасывает сам себя, не оставляя следов, и все последующие вызывающие абоненты переходят непосредственно в DynamicMethod
бездаже намек на обходную логику.
static Func<Ectx, Sctx> __get = arg =>
{
// Hijack the first caller to do initialization...
var fi = typeof(Ectx).GetField(
"_syncContext", // private field in 'ExecutionContext'
BindingFlags.NonPublic|BindingFlags.Instance);
var dm = new DynamicMethod(
"foo", // (any name)
typeof(Sctx), // getter return type
new[] { typeof(Ectx) }, // type of getter's single arg
typeof(Ectx), // "owner" type
true); // allow private field access
var il = dm.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, fi);
il.Emit(OpCodes.Ret);
// ...now replace ourself...
__get = (Func<Ectx, Sctx>)dm.CreateDelegate(typeof(Func<Ectx, Sctx>));
// oh yeah, don't forget to handle the first caller's request
return __get(arg); // ...never to come back here again. SAD!
};
Симпатичная часть - это тот самый конец, где - для того, чтобы на самом деле получить значение для упущенного первого вызывающего - функция якобы вызывает себя со своим собственнымаргумент, но избегает повторения, заменяя себя непосредственно перед.
Нет особой причины демонстрировать эту необычную технику для конкретной проблемы SynchronizationContext
, обсуждаемой на этой странице.Извлечение поля _syncContext
из ExecutionContext
может быть легко и тривиально решено с помощью традиционного отражения (плюс некоторый метод расширения мороза).Но я подумал, что поделюсь этим подходом, который я лично использовал довольно давно, потому что он также легко адаптируется и столь же широко применим к таким случаям.
Это особенно уместно, когда необходима чрезвычайная производительностьв доступе к закрытым полям.Я думаю, что первоначально я использовал это в частотном счетчике на основе QPC, где поле считывалось в узком цикле, который повторялся каждые 20 или 25 наносекунд, что было бы невозможно при обычном отражении.
На этом основной ответ заканчивается, но ниже я включил несколько интересных моментов, менее актуальных для вопроса спрашивающего, а также к только что продемонстрированной технике.
Вызовы во время выполнения
Для ясности я разделил шаги «своп установки» и «первое использование» на две отдельные строки в приведенном выше коде, в отличие от того, что у меня есть в моем собственномкод (следующая версия также позволяет избежать одной выборки из основной памяти по сравнению с предыдущей, что потенциально может повлиять на безопасность потоков, см. подробное обсуждение ниже):
return (__get = (Func<Ectx, Sctx>)dm.CreateDel...(...))(arg);
Другими словами, все вызывающие абоненты, , включая первый, получить значение точно таким же образом, и никакой код отражения никогда не используется для этого.В нем записывается только замена getter .Предоставлено il-visualizer , мы можем увидеть тело этого DynamicMethod
в отладчике во время выполнения:
ldfld SynchronizationContext _syncContext/ExecutionContext
ret">
Безопасность потоков без блокировок
Следует отметить, что замена в теле функции является полностью потокобезопасной операцией, учитывая модель памяти .NET и философию без блокировок,Последний предпочитает гарантии продвижения вперед за счет возможной дублирующей или избыточной работы.Многосторонняя гонка для инициализации правильно разрешена на полностью обоснованной теоретической основе:
- точка входа в гонку (код инициализации) предварительно настроена и защищена глобально (загрузчиком .NET), так что (несколько) гонщики (если таковые имеются) входят в один и тот же инициализатор, который никогда не может рассматриваться как
null
. - продукты нескольких рас (геттер) всегда логически идентичны, поэтому не имеет значения, какой из них какой-либо конкретныйслучается, что гонщик (или более поздний не участвующий в гонках) обнаруживает, или даже когда какой-то гонщик использует тот, который он сам создал;
- каждый установочный своп представляет собой единое хранилище размером
IntPtr
, что гарантированобыть атомарным для любой соответствующей битности платформы; - наконец, и технически абсолютно критически важно для идеальной формальной корректности , рабочие продукты "неудачников" исправляются на
GC
и, таким образом, не дают утечки. В этом типе гонки проигравшими являются все гонщики, кроме last финишера (поскольку все остальные усилия беспечно и в общем перезаписываются с одинаковым результатом).
Хотя я полагаю, что эти пункты в совокупности обеспечивают полную защиту кода, написанного при любых возможных обстоятельствах, если вы все еще сомневаетесь или опасаетесь общего вывода, вы всегда можете добавить дополнительный уровень пуленепробиваемости:
var tmp = (Func<Ectx, Sctx>)dm.CreateDelegate(typeof(Func<Ectx, Sctx>));
Thread.MemoryBarrier();
__get = tmp;
return tmp(arg);
Это просто паранойная версия. Как и в случае ранее сжатой однострочной, модель памяти .NET гарантирует, что существует ровно один магазин - и ноль выборок - в местоположение '__get'. (Полный расширенный пример в верхней части делает дополнительную выборку из основной памяти, но все еще исправен благодаря второму пункту). Как я уже упоминал, для правильности ничего из этого не должно быть необходимым, но теоретически это может дать незначительную ошибку. бонус за производительность: окончательно завершив гонку ранее , агрессивный сброс мог, в крайне редком случае, предотвратить ненужную (но опять же безвредную) гонку последующего коллера на грязной линии кэша.
Дважды thunking
Вызовы в последний, сверхбыстрый метод все еще thunked через статические методы расширения, показанные ранее.
Это потому, что нам также нужно каким-то образом представлять точку (точки) входа, которые действительно существуют во время компиляции, чтобы компилятор связывался и распространял метаданные для. Двойной принцип - это небольшая цена, которую приходится платить за подавляющее удобство строго типизированных метаданных и intellisense в IDE для настраиваемого кода, который на самом деле не может быть разрешен до времени выполнения. Тем не менее, он работает по крайней мере так же быстро, как статически скомпилированный код, способ быстрее, чем делает кучу размышлений о каждом вызове, поэтому мы получаем лучшее из обоих миров!