Что делает SynchronizationContext?

135

В книге «Программирование на C #» есть пример кода о SynchronizationContext:

SynchronizationContext originalContext = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(delegate {
    string text = File.ReadAllText(@"c:\temp\log.txt");
    originalContext.Post(delegate {
        myTextBox.Text = text;
    }, null);
});

Я новичок в темах, поэтому ответьте пожалуйста подробно. Во-первых, я не знаю, что означает контекст, что программа сохраняет в формате originalContext? И когда Postметод запускается, что будет делать поток пользовательского интерфейса?
Если я попрошу глупостей, пожалуйста, поправьте меня, спасибо!

РЕДАКТИРОВАТЬ: Например, что, если я просто напишу myTextBox.Text = text;в методе, в чем разница?

cloudyFan
источник
1
В прекрасном руководстве говорится об этом . Цель модели синхронизации, реализованной этим классом, состоит в том, чтобы позволить внутренним асинхронным / синхронизирующим операциям общеязыковой среды выполнения вести себя должным образом с различными моделями синхронизации. Эта модель также упрощает некоторые требования, которым должны следовать управляемые приложения для правильной работы в различных средах синхронизации.
ta.speot.is
IMHO async await уже делает это
Рой Намир
7
@RoyiNamir: Да, но угадайте, на что: async/ awaitполагается SynchronizationContextвнизу.
stakx - больше не вносит свой вклад

Ответы:

170

Что делает SynchronizationContext?

Проще говоря, SynchronizationContextпредставляет собой место, «где» код может быть выполнен. Затем в этом месте будут вызываться делегаты, переданные его методуSend или . ( это неблокирующая / асинхронная версияPostPostSend .)

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

Несмотря на то, что я только что написал (каждый поток имеет связанный контекст синхронизации), a SynchronizationContextне обязательно представляет конкретный поток ; он также может перенаправлять вызов переданных ему делегатов в любой из нескольких потоков (например, в ThreadPoolрабочий поток) или (по крайней мере, теоретически) на конкретное ядро ЦП или даже на другой сетевой хост . Где будут работать ваши делегаты, зависит от типа SynchronizationContextиспользуемого.

Windows Forms установит WindowsFormsSynchronizationContextв поток, в котором создается первая форма. (Этот поток обычно называют «потоком пользовательского интерфейса».) Этот тип контекста синхронизации вызывает делегатов, переданных ему именно в этом потоке. Это очень полезно, поскольку Windows Forms, как и многие другие фреймворки пользовательского интерфейса, разрешает манипулирование элементами управления только в том же потоке, в котором они были созданы.

Что если я просто напишу myTextBox.Text = text;в методе, какая разница?

Код, который вы передали, ThreadPool.QueueUserWorkItemбудет выполняться в рабочем потоке пула потоков. То есть он не будет выполняться в потоке, в котором ваш myTextBoxбыл создан, поэтому Windows Forms рано или поздно (особенно в сборках Release) выдаст исключение, сообщающее вам, что вы не можете получить доступmyTextBox из другого потока.

Вот почему вам нужно каким-то образом «переключиться обратно» с рабочего потока на «поток пользовательского интерфейса» (где myTextBoxбыл создан) перед этим конкретным назначением. Это делается следующим образом:

  1. Пока вы все еще находитесь в потоке пользовательского интерфейса, SynchronizationContextсохраните там Windows Forms и сохраните ссылку на них в переменной ( originalContext) для дальнейшего использования. Вы должны сделать запрос SynchronizationContext.Currentна этом этапе; если вы запросили его в переданном коде ThreadPool.QueueUserWorkItem, вы можете получить любой контекст синхронизации, связанный с рабочим потоком пула потоков. После того, как вы сохранили ссылку на контекст Windows Forms, вы можете использовать ее в любом месте и в любое время для «отправки» кода в поток пользовательского интерфейса.

  2. Всякий раз, когда вам нужно манипулировать элементом пользовательского интерфейса (но его нет или может не быть в потоке пользовательского интерфейса), обращайтесь к контексту синхронизации Windows Forms через originalContextи передайте код, который будет управлять пользовательским интерфейсом, либо Sendили Post.


Заключительные замечания и подсказки:

  • Что контексты синхронизации не будет делать для вас говорит вам , какой код должен работать в определенном месте / контекста, и какой код может быть просто выполнен нормально, не пропуская ее к SynchronizationContext. Чтобы решить это, вы должны знать правила и требования фреймворка, для которого вы программируете - в данном случае Windows Forms.

    Так что запомните это простое правило для Windows Forms: НЕ обращайтесь к элементам управления или формам из потока, отличного от того, который их создал. Если вы должны это сделать, используйте SynchronizationContextописанный выше механизм или Control.BeginInvoke(который является специфичным для Windows Forms способом сделать то же самое).

  • Если вы программируете на .NET 4.5 или более поздней версии, вы можете сделать вашу жизнь намного проще путем преобразования кода , который явно использует SynchronizationContext, ThreadPool.QueueUserWorkItem, control.BeginInvokeи т.д. к новым async/ awaitключевых слов и Task Parallel Library (TPL) , то есть API окружающей Taskи Task<TResult>классы. Они в очень высокой степени позаботятся о захвате контекста синхронизации потока пользовательского интерфейса, запуске асинхронной операции, а затем возвращении в поток пользовательского интерфейса, чтобы вы могли обработать результат операции.

Stakx - больше не помогает
источник
Вы говорите, что Windows Forms, как и многие другие инфраструктуры пользовательского интерфейса, разрешает манипулирование элементами управления только в одном потоке, но все окна в Windows должны быть доступны для того же потока, который их создал.
user34660
4
@ user34660: Нет, это не так. У вас может быть несколько потоков, которые создают элементы управления Windows Forms. Но каждый элемент управления связан с одним потоком, который его создал, и должен быть доступен только этому одному потоку. Элементы управления из разных потоков пользовательского интерфейса также очень ограничены в том, как они взаимодействуют друг с другом: один не может быть родительским / дочерним для другого, привязка данных между ними невозможна и т.д. Наконец, каждый поток, который создает элементы управления, нуждается в собственном сообщении. цикл (который запускается Application.RunIIRC). Это довольно сложная тема, и она не происходит случайно.
stakx - больше не вносит свой вклад
Мой первый комментарий связан с тем, что вы сказали «как и многие другие фреймворки пользовательского интерфейса», подразумевая, что некоторые окна позволяют «манипулировать элементами управления» из другого потока, а окна Windows - нет. Вы не можете «иметь несколько потоков, которые создают элементы управления Windows Forms» для одного и того же окна, и «должны быть доступны одному и тому же потоку» и «должны быть доступны только этому одному потоку» говорят об одном и том же. Я сомневаюсь, что можно создавать «Элементы управления из разных потоков пользовательского интерфейса» для одного и того же окна. Все это не является продвинутым для тех из нас, кто имел опыт программирования для Windows до .Net.
user34660
3
Все эти разговоры о «окнах» и «окнах Windows» вызывают у меня головокружение. Я упоминал какое-нибудь из этих «окон»? Я так не думаю ...
stakx - больше не участвует
1
@ibubi: Я не уверен, что понимаю ваш вопрос. Контекст синхронизации любого потока либо не установлен ( null), либо не является его экземпляром SynchronizationContext(или его подклассом). Смысл этой цитаты был не в том, что вы получите, а в том, чего вы не получите: контекст синхронизации потока пользовательского интерфейса.
stakx - больше не участвует
24

Я хотел бы добавить к другим ответам, SynchronizationContext.Postпросто ставит в очередь обратный вызов для последующего выполнения в целевом потоке (обычно во время следующего цикла цикла сообщений целевого потока), а затем выполнение продолжается в вызывающем потоке. С другой стороны, SynchronizationContext.Sendпытается немедленно выполнить обратный вызов в целевом потоке, что блокирует вызывающий поток и может привести к тупиковой ситуации. В обоих случаях существует возможность повторного входа в код (ввод метода класса в том же потоке выполнения до возврата из предыдущего вызова того же метода).

Если вы знакомы с моделью программирования Win32, очень близкая аналогия была бы PostMessageи SendMessageAPI, которые вы можете вызвать к отправке сообщений из другого потока от одного целевого окна.

Вот очень хорошее объяснение того, что такое контексты синхронизации: Все о контексте синхронизации .

noseratio
источник
16

В нем хранится поставщик синхронизации, класс, производный от SynchronizationContext. В этом случае это, вероятно, будет экземпляр WindowsFormsSynchronizationContext. Этот класс использует методы Control.Invoke () и Control.BeginInvoke () для реализации методов Send () и Post (). Или это может быть DispatcherSynchronizationContext, он использует Dispatcher.Invoke () и BeginInvoke (). В приложении Winforms или WPF этот провайдер устанавливается автоматически, как только вы создаете окно.

Когда вы запускаете код в другом потоке, например поток пула потоков, используемый во фрагменте, вы должны быть осторожны, чтобы напрямую не использовать объекты, небезопасные для потоков. Как и любой объект пользовательского интерфейса, вы должны обновить свойство TextBox.Text из потока, создавшего TextBox. Метод Post () гарантирует, что целевой делегат работает в этом потоке.

Помните, что этот фрагмент немного опасен, он будет работать правильно только тогда, когда вы вызовете его из потока пользовательского интерфейса. SynchronizationContext.Current имеет разные значения в разных потоках. Только поток пользовательского интерфейса имеет полезное значение. И это причина, по которой код должен был его скопировать. Более читаемый и безопасный способ сделать это в приложении Winforms:

    ThreadPool.QueueUserWorkItem(delegate {
        string text = File.ReadAllText(@"c:\temp\log.txt");
        myTextBox.BeginInvoke(new Action(() => {
            myTextBox.Text = text;
        }));
    });

Это имеет то преимущество, что он работает при вызове из любого потока. Преимущество использования SynchronizationContext.Current заключается в том, что он по-прежнему работает независимо от того, используется ли код в Winforms или WPF, это имеет значение в библиотеке. Это, конечно, не лучший пример такого кода, вы всегда знаете, какой у вас TextBox, поэтому вы всегда знаете, использовать ли Control.BeginInvoke или Dispatcher.BeginInvoke. На самом деле использование SynchronizationContext.Current не так уж часто.

Книга пытается научить вас многопоточности, поэтому использовать этот ошибочный пример - нормально. В реальной жизни в тех немногих случаях, когда вы могли бы рассмотреть возможность использования SynchronizationContext.Current, вы все равно оставите это на усмотрение ключевых слов async / await C # или TaskScheduler.FromCurrentSynchronizationContext (), чтобы сделать это за вас. Но обратите внимание, что они по-прежнему ведут себя плохо, как фрагмент, когда вы используете их не в том потоке, по той же причине. Очень распространенный вопрос здесь: дополнительный уровень абстракции полезен, но затрудняет понимание, почему они не работают правильно. Надеюсь, книга также подскажет, когда ее не использовать :)

Ганс Пассант
источник
Прошу прощения, зачем позволять дескриптору потока пользовательского интерфейса быть потокобезопасным? т.е. я думаю, что поток пользовательского интерфейса может использовать myTextBox при запуске Post (), это безопасно?
cloudyFan
4
Твой английский сложно расшифровать. Ваш исходный фрагмент кода работает правильно только тогда, когда он вызывается из потока пользовательского интерфейса. Это очень частый случай. Только после этого он будет отправлен обратно в поток пользовательского интерфейса. Если он вызывается из рабочего потока, то целевой делегат Post () будет работать в потоке пула потоков. Kaboom. Это то, что вы хотите попробовать сами. Запустите поток и позвольте потоку вызвать этот код. Вы сделали все правильно, если код вылетает с исключением NullReferenceException.
Ханс Пассан
5

Цель контекста синхронизации здесь - убедиться, что он вызывается myTextbox.Text = text;в основном потоке пользовательского интерфейса.

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

При этом сохраняется контекст синхронизации перед созданием фонового потока, затем фоновый поток использует метод context.Post для выполнения кода графического интерфейса.

Да, код, который вы показали, в основном бесполезен. Зачем создавать фоновый поток только для того, чтобы немедленно вернуться к основному потоку пользовательского интерфейса? Это просто пример.

Эрик Фанкенбуш
источник
4
«Да, код, который вы показали, в основном бесполезен. Зачем создавать фоновый поток только для того, чтобы немедленно вернуться к основному потоку пользовательского интерфейса? Это просто пример». - Чтение из файла может оказаться долгой задачей, если файл большой, что-то, что может заблокировать поток пользовательского интерфейса и сделать его невосприимчивым
Яир Невет
У меня глупый вопрос. Каждый поток имеет идентификатор, и я полагаю, что поток пользовательского интерфейса также имеет идентификатор, например, = 2. Затем, когда я нахожусь в потоке пула потоков, могу ли я сделать что-то вроде этого: var thread = GetThread (2); thread.Execute (() => textbox1.Text = "foo")?
Джон
@John - Нет, я не думаю, что это сработает, потому что поток уже выполняется. Вы не можете выполнить уже выполняющийся поток. Execute работает только тогда, когда поток не запущен (IIRC)
Эрик
3

К источнику

Каждый поток имеет связанный с ним контекст - он также известен как «текущий» контекст - и эти контексты могут совместно использоваться потоками. ExecutionContext содержит соответствующие метаданные текущей среды или контекста, в котором выполняется программа. SynchronizationContext представляет собой абстракцию - он обозначает место, где выполняется код вашего приложения.

SynchronizationContext позволяет вам поставить задачу в очередь в другой контекст. Обратите внимание, что каждый поток может иметь собственный контекст SynchronizatonContext.

Например: Предположим, у вас есть два потока, Thread1 и Thread2. Скажем, Thread1 выполняет некоторую работу, а затем Thread1 хочет выполнить код в Thread2. Один из возможных способов сделать это - запросить у Thread2 его объект SynchronizationContext, передать его Thread1, а затем Thread1 может вызвать SynchronizationContext.Send для выполнения кода в Thread2.

Большие глаза
источник
2
Контекст синхронизации не обязательно привязан к конкретному потоку. Возможно, что несколько потоков обрабатывают запросы к одному контексту синхронизации, а один поток может обрабатывать запросы для нескольких контекстов синхронизации.
Servy
3

SynchronizationContext предоставляет нам способ обновить пользовательский интерфейс из другого потока (синхронно с помощью метода Send или асинхронно с помощью метода Post).

Взгляните на следующий пример:

    private void SynchronizationContext SyncContext = SynchronizationContext.Current;
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Thread thread = new Thread(Work1);
        thread.Start(SyncContext);
    }

    private void Work1(object state)
    {
        SynchronizationContext syncContext = state as SynchronizationContext;
        syncContext.Post(UpdateTextBox, syncContext);
    }

    private void UpdateTextBox(object state)
    {
        Thread.Sleep(1000);
        string text = File.ReadAllText(@"c:\temp\log.txt");
        myTextBox.Text = text;
    }

SynchronizationContext.Current вернет контекст синхронизации потока пользовательского интерфейса. Откуда я это знаю? В начале каждой формы или приложения WPF контекст будет установлен в потоке пользовательского интерфейса. Если вы создадите приложение WPF и запустите мой пример, вы увидите, что когда вы нажмете кнопку, оно спит примерно 1 секунду, а затем отобразит содержимое файла. Вы можете ожидать, что этого не произойдет, потому что вызывающий метод UpdateTextBox (которым является Work1) - это метод, переданный потоку, поэтому он должен засыпать этот поток, а не основной поток пользовательского интерфейса, NOPE! Хотя метод Work1 передается в поток, обратите внимание, что он также принимает объект, который является SyncContext. Если вы посмотрите на него, вы увидите, что метод UpdateTextBox выполняется через метод syncContext.Post, а не через метод Work1. Взгляните на следующее:

private void Button_Click(object sender, RoutedEventArgs e) 
{
    Thread.Sleep(1000);
    string text = File.ReadAllText(@"c:\temp\log.txt");
    myTextBox.Text = text;
}

Последний пример и этот выполняются одинаково. Оба не блокируют пользовательский интерфейс, пока он выполняет свою работу.

В заключение представьте, что SynchronizationContext - это поток. Это не поток, он определяет поток (обратите внимание, что не все потоки имеют SyncContext). Всякий раз, когда мы вызываем для него метод Post или Send для обновления пользовательского интерфейса, это похоже на обновление пользовательского интерфейса обычно из основного потока пользовательского интерфейса. Если по каким-либо причинам вам необходимо обновить пользовательский интерфейс из другого потока, убедитесь, что этот поток имеет SyncContext основного потока пользовательского интерфейса, и просто вызовите для него метод Send или Post с методом, который вы хотите выполнить, и все устанавливать.

Надеюсь, это поможет тебе, дружище!

Marc2001
источник
2

SynchronizationContext в основном является поставщиком выполнения делегатов обратного вызова, в основном отвечающим за то, чтобы делегаты запускались в заданном контексте выполнения после того, как определенная часть кода (инкапсулированная в Task obj .Net TPL) программы завершила свое выполнение.

С технической точки зрения SC - это простой класс C #, ориентированный на поддержку и предоставление своих функций специально для объектов параллельной библиотеки задач.

Каждое приложение .Net, за исключением консольных приложений, имеет конкретную реализацию этого класса, основанную на конкретной базовой структуре, например: WPF, WindowsForm, Asp Net, Silverlight и т. Д.

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

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

Чиро Корвино
источник
1

Этот пример взят из примеров Linqpad от Джозефа Альбахари, но он действительно помогает понять, что делает контекст синхронизации.

void WaitForTwoSecondsAsync (Action continuation)
{
    continuation.Dump();
    var syncContext = AsyncOperationManager.SynchronizationContext;
    new Timer (_ => syncContext.Post (o => continuation(), _)).Change (2000, -1);
}

void Main()
{
    Util.CreateSynchronizationContext();
    ("Waiting on thread " + Thread.CurrentThread.ManagedThreadId).Dump();
    for (int i = 0; i < 10; i++)
        WaitForTwoSecondsAsync (() => ("Done on thread " + Thread.CurrentThread.ManagedThreadId).Dump());
}
loneshark99
источник