Получить SynchronizationContext из данного потока - PullRequest
7 голосов
/ 05 ноября 2010

Кажется, я не могу найти, как получить SynchronizationContext данного Thread:

Thread uiThread = UIConfiguration.UIThread;
SynchronizationContext context = uiThread.Huh?;

Зачем мне это нужно?

Потому что мне нужно публиковать в UIThread из разных мест во всем приложении. Поэтому я определил статическое свойство в классе с именем UIConfiguration. Я устанавливаю это свойство в методе Program.Main:

UIConfiguration.UIThread = Thread.CurrentThread;

В тот самый момент я могу быть уверен, что у меня есть нужный поток, однако я не могу установить статическое свойство, такое как

UIConfiguration.SynchronizationContext = SynchronizationContext.Current

потому что реализация WinForms этого класса еще не установлена. Поскольку каждый поток имеет свой собственный SynchronizationContext, должна быть возможность извлечь его из данного Thread объекта, или я совершенно не прав?

Ответы [ 5 ]

12 голосов
/ 05 ноября 2010

Это невозможно. Проблема в том, что SynchronizationContext и Thread - это две совершенно разные концепции.

Хотя Windows Forms и WPF действительно устанавливают SynchronizationContext для основного потока, большинство других потоков этого не делают. Например, ни один из потоков в ThreadPool не содержит своего собственного SynchronizationContext (если, конечно, вы не установили свой собственный).

Возможно также, что SynchronizationContext будет полностью не связанным с потоками и потоками . Можно легко настроить контекст синхронизации, который синхронизируется с внешней службой или с целым пулом потоков и т. Д.

В вашем случае я бы порекомендовал установить UIConfiguration.SynchronizationContext в рамках начального события Loaded главной формы. С этой точки зрения гарантированно запускается контекст, и он будет недоступен до тех пор, пока в любом случае не будет запущен насос сообщений.

7 голосов
/ 17 апреля 2011

Я знаю, что это старый вопрос, и прошу прощения за некро, но я только что нашел решение этой проблемы, которое, как я подумал, могло бы быть полезным для тех из нас, кто гуглил это (и не требует контроля экземпляр).

По сути, вы можете создать экземпляр WindowsFormsSynchronizationContext и установить контекст вручную в вашей функции Main, например:

    _UISyncContext = new WindowsFormsSynchronizationContext();
    SynchronizationContext.SetSynchronizationContext(_UISyncContext);

Я сделал это в своем приложении, и оно работает без проблем. Тем не менее, я должен отметить, что мой Main помечен STAThread, поэтому я не уверен, будет ли он все еще работать (или даже будет ли это необходимо), если ваш Main помечен как MTAThread.

РЕДАКТИРОВАТЬ: я забыл упомянуть об этом, но _UISyncContext уже определен на уровне модуля в классе Program в моем приложении.

4 голосов

Я нашел наиболее краткими и полезными следующие отрывки из книги Алекса Дэвиса «Асинхронность в C # 5.0. O'Reilly Publ., 2012», с. 48-49:

  • " SynchronizationContext - это класс, предоставляемый .NET Framework , который может запускать код в конкретном типе потока .
    . Существуют различные контексты синхронизации.используется .NET, наиболее важными из которых являются контексты потоков пользовательского интерфейса, используемые WinForms и WPF. "

  • "Экземпляры SynchronizationContext сами по себе не делают ничего очень полезного, поэтому все их реальные экземпляры, как правило, являются подклассами.

    В нем также есть статические члены, которыепозволяет вам читать и контролировать текущий SynchronizationContext.

    Текущий SynchronizationContext является свойством текущего потока.

    Идея состоит в том, что в любой точкевы работаете в специальном потоке, вы должны иметь возможность получить текущий SynchronizationContext и сохранить его. Позже вы можете использовать его для запуска кода обратно в специальном потоке, в котором вы начали. Все это должно быть возможно без необходимости точно знать, с какого потока вы начали, пока вы можете использовать SynchronizationContext, вы можете вернуться к нему .

    Важным методом SynchronizationContext является Post, который может сделатьзапуск делегата в правильном контексте "
    .

  • " Некоторые SynchronizationContexts инкапсулируют один поток, например поток пользовательского интерфейса .
    Некоторые инкапсулируют определенный тип потока - например, пул потоков - но может выбрать любой из этих потоков для отправки делегата в . Некоторые на самом деле не изменяют, в каком потоке выполняется код, но используются только для мониторинга, например, для синхронизации ASP.NETКонтекст "

2 голосов
/ 05 ноября 2010

Я не верю, что каждый поток имеет свой собственный SynchronizationContext - он просто имеет локальный поток SynchronizationContext.

Почему бы вам просто не установить UIConfiguration.UIThread в Loaded событии вашей формы или что-то подобное?

1 голос
/ 08 июля 2017

Полные и рабочие методы расширения для получения 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 в отладчике во время выполнения:

imageldfld 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 для настраиваемого кода, который на самом деле не может быть разрешен до времени выполнения. Тем не менее, он работает по крайней мере так же быстро, как статически скомпилированный код, способ быстрее, чем делает кучу размышлений о каждом вызове, поэтому мы получаем лучшее из обоих миров!

...