Подтвердить что ты не робот

Разница между контекстом синхронизации и диспетчером

Я использую Dispatcher для переключения на поток пользовательского интерфейса из внешнего типа

Application.Current.Dispatcher.Invoke(myAction);

Но я видел, что на некоторых форумах люди советовали использовать Synchronization контекст вместо Dispatcher.

SynchronizationContext.Current.Post(myAction,null);

В чем разница между ними и почему следует использовать SynchronizationContext?

4b9b3361

Ответ 1

Оба они имеют схожие эффекты, но SynchronizationContext более общий.

Application.Current.Dispatcher относится к диспетчеру WPF приложения и использует Invoke который выполняет делегат в основном потоке этого приложения.

SynchronizationContext.Current другой стороны, SynchronizationContext.Current возвращает различные реализации в зависимости от текущего потока. Когда вызывается в потоке пользовательского интерфейса приложения WPF, он возвращает SynchronizationContext который использует диспетчера при вызове в потоке пользовательского интерфейса приложения WinForms, который возвращает другой.

Вы можете увидеть классы, наследующие от SynchronizationContext в документации MSDN: WindowsFormsSynchronizationContext и DispatcherSynchronizationContext.


Одна вещь, о которой нужно знать при использовании SynchronizationContext заключается в том, что она возвращает контекст синхронизации текущего потока. Если вы хотите использовать контекст синхронизации другого потока, например поток пользовательского интерфейса, вы должны сначала получить его контекст и сохранить его в переменной:

public void Control_Event(object sender, EventArgs e)
{
    var uiContext = SynchronizationContext.Current;
    Task.Run(() => 
    {
        // do some work
        uiContext.Post(/* update UI controls*/);
    }
}

Это не относится к Application.Current.Dispatcher, который всегда возвращает диспетчер для приложения.

Ответ 2

При использовании WPF объект SynchronizationContext.Current имеет тип DispatcherSynchronizationContext, который фактически является оберткой вокруг объекта Dispatcher, а методы Post и Send просто делегируются Dispatcher.BeginInvoke и Dispatcher.Invoke.

Итак, даже если вы решите использовать SynchronizationContext, я думаю, что вы в конечном итоге вызываете диспетчера за кулисами.

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

Ответ 3

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

  1. Он предоставляет возможность помещать единицу работы в контекст. Обратите внимание, что это не зависит от потока, и поэтому мы избегаем проблемы с привязкой потоков.
  2. Каждый поток имеет "текущий" контекст, но этот контекст может быть разделен между потоками, т.е. Контекст не обязательно уникален.
  3. Контексты содержат количество выдающихся асинхронных операций. Этот счет часто, но не всегда увеличивается/уменьшается на захват/очередь.

Поэтому, чтобы ответить на ваш вопрос о том, какой из них выбрать, казалось бы, из критериев выше, что использование SynchronizationContext было бы предпочтительнее Диспетчера.

Но есть еще более веские причины:

  • Разделение проблем

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

  • Простое модульное тестирование

Если вы когда-либо пытались издеваться над таким сложным объектом, как Dispatcher и SynchronizationContext, который имеет гораздо меньше методов для решения, вы быстро поймете гораздо более простой интерфейс, предлагаемый SynchronizationContext.

  • IoC и инъекция зависимостей

Как вы уже видели, SynchronizationContext реализован во многих инфраструктурах пользовательского интерфейса: WinForms, WPF, ASP.NET и т.д. Если вы напишете свой код для взаимодействия с одним набором API, ваш код станет более переносимым и более простым в обслуживании, а также тестовое задание.

Вам не нужно даже вводить объект контекста... вы можете вводить любой объект с интерфейсом, который соответствует методам объекта контекста, включая прокси.

В качестве примера:

Примечание. Я исключил обработку исключений, чтобы сделать код ясным.

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

Используя WPF и традиционный подход Dispatch, вы можете написать что-то вроде этого:

    /// <summary>
    /// Start a long series of asynchronous tasks using the Dispatcher for coordinating
    /// UI updates.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Start_Via_Dispatcher_OnClick(object sender, RoutedEventArgs e)
    {
        // update initial start time and task status
        Time_Dispatcher.Text = DateTime.Now.ToString("hh:mm:ss");
        Status_Dispatcher.Text = "Started";

        // create UI dont event object
        var uiUpdateDone = new ManualResetEvent(false);

        // Start a new task (this uses the default TaskScheduler, 
        // so it will run on a ThreadPool thread).
        Task.Factory.StartNew(async () =>
        {
            // We are running on a ThreadPool thread here.

            // Do some work.
            await Task.Delay(2000);

            // Report progress to the UI.
            Application.Current.Dispatcher.Invoke(() =>
            {
                Time_Dispatcher.Text = DateTime.Now.ToString("hh:mm:ss");

                // signal that update is complete
                uiUpdateDone.Set();
            });

            // wait for UI thread to complete and reset event object
            uiUpdateDone.WaitOne();
            uiUpdateDone.Reset();

            // Do some work.
            await Task.Delay(2000); // Do some work.

            // Report progress to the UI.
            Application.Current.Dispatcher.Invoke(() =>
            {
                Time_Dispatcher.Text = DateTime.Now.ToString("hh:mm:ss");

                // signal that update is complete
                uiUpdateDone.Set();
            });

            // wait for UI thread to complete and reset event object
            uiUpdateDone.WaitOne();
            uiUpdateDone.Reset();

            // Do some work.
            await Task.Delay(2000); // Do some work.

            // Report progress to the UI.
            Application.Current.Dispatcher.Invoke(() =>
            {
                Time_Dispatcher.Text = DateTime.Now.ToString("hh:mm:ss");

                // signal that update is complete
                uiUpdateDone.Set();
            });

            // wait for UI thread to complete and reset event object
            uiUpdateDone.WaitOne();
            uiUpdateDone.Reset();
        },
        CancellationToken.None,
        TaskCreationOptions.None,
        TaskScheduler.Default)
            .ConfigureAwait(false)
            .GetAwaiter()
            .GetResult()
            .ContinueWith(_ =>
            {
                Application.Current.Dispatcher.Invoke(() =>
                {
                    Status_Dispatcher.Text = "Finished";

                    // dispose of event object
                    uiUpdateDone.Dispose();
                });
            });
    }

Этот код работает по назначению, но имеет следующие недостатки:

  1. Код привязан к объекту Диспетчер приложений WPF. Это затрудняет единичный тест и реферат.
  2. Необходимость внешнего объекта ManualResetEvent для синхронизации между потоками. Это должно немедленно исправить запах кода, так как теперь это зависит от другого ресурса, который нужно издеваться.
  3. Сложность в управлении временем жизни объекта для одного и того же объекта ядра.

Теперь попробуйте еще раз, используя объект SynchronizationContext:

    /// <summary>
    /// 
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Start_Via_SynchronizationContext_OnClick(object sender, RoutedEventArgs e)
    {
        // update initial time and task status
        Time_SynchronizationContext.Text = DateTime.Now.ToString("hh:mm:ss");
        Status_SynchronizationContext.Text = "Started";

        // capture synchronization context
        var sc = SynchronizationContext.Current;

        // Start a new task (this uses the default TaskScheduler, 
        // so it will run on a ThreadPool thread).
        Task.Factory.StartNew(async () =>
        {
            // We are running on a ThreadPool thread here.

            // Do some work.
            await Task.Delay(2000);

            // Report progress to the UI.
            sc.Send(state =>
            {
                Time_SynchronizationContext.Text = DateTime.Now.ToString("hh:mm:ss");
            }, null);

            // Do some work.
            await Task.Delay(2000);

            // Report progress to the UI.
            sc.Send(state =>
            {
                Time_SynchronizationContext.Text = DateTime.Now.ToString("hh:mm:ss");
            }, null);

            // Do some work.
            await Task.Delay(2000);

            // Report progress to the UI.
            sc.Send(state =>
            {
                Time_SynchronizationContext.Text = DateTime.Now.ToString("hh:mm:ss");
            }, null);
        },
        CancellationToken.None,
        TaskCreationOptions.None,
        TaskScheduler.Default)
        .ConfigureAwait(false)
        .GetAwaiter()
        .GetResult()
        .ContinueWith(_ =>
        {
            sc.Post(state =>
            {
                Status_SynchronizationContext.Text = "Finished";
            }, null);
        });
    }

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

Теперь, даже если вы не спрашивали, но для полноты, есть еще один способ выполнить то, что вы хотите абстрактным образом, без необходимости в объекте SynchronizationContext или с помощью Диспетчера. Поскольку мы уже используем TPL (параллельную библиотеку задач) для нашей обработки задач, мы могли бы просто использовать планировщик задач следующим образом:

    /// <summary>
    /// 
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Start_Via_TaskScheduler_OnClick(object sender, RoutedEventArgs e)
    {
        Time_TaskScheduler.Text = DateTime.Now.ToString("hh:mm:ss");


        // This TaskScheduler captures SynchronizationContext.Current.
        var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
        Status_TaskScheduler.Text = "Started";

        // Start a new task (this uses the default TaskScheduler, 
        // so it will run on a ThreadPool thread).
        Task.Factory.StartNew(async () =>
        {
            // We are running on a ThreadPool thread here.

            // Do some work.
            await Task.Delay(2000);

            // Report progress to the UI.
            var reportProgressTask = ReportProgressTask(taskScheduler, () =>
            {
                Time_TaskScheduler.Text = DateTime.Now.ToString("hh:mm:ss");
                return 90;
            });

            // get result from UI thread
            var result = reportProgressTask.Result;
            Debug.WriteLine(result);

            // Do some work.
            await Task.Delay(2000); // Do some work.

            // Report progress to the UI.
            reportProgressTask = ReportProgressTask(taskScheduler, () =>
                {
                    Time_TaskScheduler.Text = DateTime.Now.ToString("hh:mm:ss");
                    return 10;
                });

            // get result from UI thread
            result = reportProgressTask.Result;
            Debug.WriteLine(result);

            // Do some work.
            await Task.Delay(2000); // Do some work.

            // Report progress to the UI.
            reportProgressTask = ReportProgressTask(taskScheduler, () =>
            {
                Time_TaskScheduler.Text = DateTime.Now.ToString("hh:mm:ss");
                return 340;
            });

            // get result from UI thread
            result = reportProgressTask.Result;
            Debug.WriteLine(result);
        }, 
        CancellationToken.None,
        TaskCreationOptions.None,
        TaskScheduler.Default)
            .ConfigureAwait(false)
            .GetAwaiter()
            .GetResult()
            .ContinueWith(_ =>
            {
                var reportProgressTask = ReportProgressTask(taskScheduler, () =>
                {
                    Status_TaskScheduler.Text = "Finished";
                    return 0;
                });
                reportProgressTask.Wait();
            });
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="taskScheduler"></param>
    /// <param name="func"></param>
    /// <returns></returns>
    private Task<int> ReportProgressTask(TaskScheduler taskScheduler, Func<int> func)
    {
        var reportProgressTask = Task.Factory.StartNew(func,
            CancellationToken.None,
            TaskCreationOptions.None,
            taskScheduler);
        return reportProgressTask;
    }

Как говорится, есть несколько способов запланировать задачу; )

Ответ 4

Класс Dispatcher полезен, когда вы уверены, что вызываете в контексте потока пользовательского интерфейса, а SynchronizationContext полезен, когда вы не уверены.

Если вы получите свой Dispatcher класс, используя метод static Dispatcher.CurrentDispatcher для некоторого потока, отличного от UI, и вызовите метод BeginInvoke, ничего не произойдет (без исключения, без предупреждения, nada).

Однако, если вы получите свой SynchronizationContext класс с помощью статического метода SynchronizationContext.Current, он вернет значение null, если поток не является потоком пользовательского интерфейса. Это перо очень полезно, потому что оно позволяет вам реагировать как на поток пользовательского интерфейса, так и на не-UI-поток соответственно.

Ответ 5

SynchronizationContext - это абстракция, использующая виртуальные методы. Использование SynchronizationContext позволяет не привязывать вашу реализацию к определенной структуре.

Пример: Windows Forms использует WindowsFormSynchronizationContext, который переопределяет сообщение для вызова Control.BeginInvoke. WPF использует тип DispatcherSynchronizationContext, который переопределяет Post для вызова Dispatcher.BeginInvoke. Вы можете проектировать свой компонент, который использует SynchronizationContext и не привязывает реализацию к определенной структуре.

Ответ 6

"Класс Dispatcher полезен, когда вы уверены, что вызываете в контексте потока пользовательского интерфейса" - вы используете диспетчер для перехода к потоку пользовательского интерфейса из потока неинтерфейса. Это позиция BeginInvoke. Если вы вызываете его из потока пользовательского интерфейса, то ваш код просто выполняется в потоке пользовательского интерфейса. Нет контекстного переключателя. Если вы не хотите называть BeginInvoke, находясь в потоке пользовательского интерфейса, вы можете вызвать Dispatcher.Current.CheckAccess(), который возвращает false, если вы не находитесь в потоке графического интерфейса пользователя