同步上下文和调度程序之间的区别

本文关键字:区别 之间 调度程序 上下文 同步 | 更新日期: 2023-09-27 18:12:13

我正在使用Dispatcher从外部切换到UI线程,就像这个一样

Application.Current.Dispatcher.Invoke(myAction);

但我在一些论坛上看到,人们建议使用SynchronizationContext而不是Dispatcher

SynchronizationContext.Current.Post(myAction,null);

它们之间有什么区别,为什么要使用SynchronizationContext?。

同步上下文和调度程序之间的区别

它们都有相似的效果,但SynchronizationContext更通用。

Application.Current.Dispatcher指的是应用程序的WPF调度器,在上使用Invoke在该应用程序的主线程上执行委托。

CCD_ 8根据当前线程返回的不同实现。当在WPF应用程序的UI线程上调用时,它会返回一个使用调度器的SynchronizationContext,当在WinForms应用程序的用户界面线程上调用它时,它返回一个不同的CCD_ 9。

您可以在其MSDN文档中看到从SynchronizationContext继承的类:WindowsFormsSynchronizationContext和DispatcherSynchronizationContext。


使用SynchronizationContext时需要注意的一点是,它返回当前线程的同步上下文。如果你想使用另一个线程的同步上下文,例如UI线程,你必须首先获取它的上下文并将其存储在一个变量中:

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,它总是返回应用程序的调度程序。

使用WPF时,SynchronizationContext.Current对象的类型为DispatcherSynchronizationContext,它实际上只是Dispatcher对象的包装器,PostSend方法只是委托给Dispatcher.BeginInvokeDispatcher.Invoke

所以,即使您决定使用SynchronizationContext,我认为您最终还是会在幕后呼叫调度员。

此外,我认为使用SynchronizationContext有点麻烦,因为您必须将对当前上下文的引用传递给所有需要调用UI的线程。

虽然已经指出了差异,但我真的看不出选择其中一个而不是另一个的原因。因此,也许这将有助于解释SynchronizationContext对象首先试图解决的问题:

  1. 它提供了一种将工作单元排队到上下文的方法。请注意,这不是特定于线程的,因此我们避免了线程亲和性的问题
  2. 每个线程都有一个"当前"上下文,但该上下文可能在线程之间共享,即上下文不一定是唯一的
  3. 上下文保持未完成异步操作的计数。此计数通常会在捕获/排队时递增/递减,但并不总是如此

因此,要回答您选择哪一个的问题,从上面的标准来看,使用SynchronizationContext比使用Dispatcher更可取。

但这样做还有更令人信服的理由:

  • 关注点分离

通过使用SynchronizationContext处理UI线程上的执行代码,您现在可以通过解耦的接口轻松地将操作与显示分离。这就引出了下一点:

  • 更简单的单元测试

如果您曾经尝试过模拟像Dispatcher这样复杂的对象,而SynchronizationContext需要处理的方法要少得多,那么您很快就会意识到SynchronizationContext提供的界面要简单得多。

  • IoC和依赖注入

正如您已经看到的,SynchronizationContext是在许多UI框架中实现的:WinForms、WPF、ASP.NET等。如果您将代码编写为与一组API接口,您的代码将变得更可移植,维护和测试也更简单。

您甚至不需要注入上下文对象。。。您可以为任何对象注入一个与上下文对象上的方法匹配的接口,包括代理。

例如:

注意:为了使代码清晰明了,我省略了异常处理

假设我们有一个WPF应用程序,它只有一个按钮。单击该按钮后,您将开始一个漫长的异步工作任务过程,这些任务与UI更新交织在一起,您需要协调两者之间的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. 该代码绑定到WPFApplicationDispatcher对象。这使得单元测试和抽象变得困难
  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对象或使用Dispatcher。由于我们已经在使用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;
}

正如他们所说,安排任务的方法不止一种;(

SynchronizationContext是一个使用虚拟方法的抽象。使用SynchronizationContext可以使您不必将实现绑定到特定的框架。

示例:Windows窗体使用覆盖Post的WindowsFormSynchronizationContext来调用Control.BeginInvoke。WPF使用覆盖Post的DispatcherSynchronizationContext类型来调用Dispatcher.BeginInvoke。您可以设计使用SynchronizationContext并且不将实现绑定到特定框架的组件。

当您确信您在用户界面线程的上下文中调用时,Dispatcher类很有用,而当您不太确定时,CCD-45很有用。

如果在某个非UI线程上使用static Dispatcher.CurrentDispatcher方法获得Dispatcher类,并调用BeginInvoke方法,则不会发生任何事情(无异常,无警告,nada(。

但是,如果您通过静态SynchronizationContext.Current方法获得SynchronizationContext类,那么如果线程不是UI线程,它将返回null。这个羽毛非常有用,因为它允许您对UI线程和非UI线程做出相应的反应。