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

我正在使用Dispatcher从外部切换到UI线程

 Application.Current.Dispatcher.Invoke(myAction); 

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

 SynchronizationContext.Current.Post(myAction,null); 

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

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

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

另一方面, SynchronizationContext.Current返回取决于当前线程的不同实现。 当在WPF应用程序的UI线程上调用它时,它返回一个使用调度程序的SynchronizationContext ,当在WinForms应用程序的UI线程上调用它时,它返回另一个。

您可以在其MSDN文档中看到inheritance自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 ,它始终返回应用程序的调度程序。

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

因此,即使您决定使用SynchronizationContext我认为您最终会在幕后调用调度程序。

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

虽然已经指出了差异,但我并没有真正看到在这里明确说明选择一个而不是另一个的原因。 因此,或许有助于解释SynchronizationContext对象首先尝试解决的问题:

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

因此,为了回答您选择哪一个的问题,从上面的标准来看,使用SynchronizationContext将优于Dispatcher。

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

  • 关注点分离

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

  • 更简单的unit testing

如果您曾尝试模拟像Dispatcher和SynchronizationContext这样复杂的对象,它只需要很少的方法来处理,您将很快体会到SynchronizationContext提供的更简单的接口。

  • IoC和dependency injection

正如您已经看到的,SynchronizationContext是在许多UI框架中实现的:WinForms,WPF,ASP.NET等。如果您编写代码以连接到一组API,那么您的代码将变得更加便携,维护更简单。测试。

您甚至不需要注入上下文对象…您可以使用与上下文对象上的方法匹配的接口(包括代理)注入任何对象。

举例来说:

注意:我遗漏了exception处理以使代码清晰。

假设我们有一个只有一个按钮的WPF应用程序。 单击该按钮后,您将开始与UI更新交错的异步工作任务的长时间过程,并且您需要在两者之间协调IPC。

使用WPF和传统的Dispatch方法,您可能会编写如下代码:

  ///  /// Start a long series of asynchronous tasks using the Dispatcher for coordinating /// UI updates. ///  ///  ///  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 Application Dispatcher对象。 这使得unit testing和抽象难以实现。
  2. 需要外部ManualResetEvent对象来在线程之间进行同步。 这应该立即引起代码气味,因为这现在取决于需要模拟的另一个资源。
  3. 管理所述相同内核对象的对象生存期的困难。

现在,让我们使用SynchronizationContext对象再次尝试:

  ///  /// ///  ///  ///  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(任务并行库)进行任务处理,因此我们可以按如下方式使用任务调度程序:

  ///  /// ///  ///  ///  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(); }); } ///  /// ///  ///  ///  ///  private Task ReportProgressTask(TaskScheduler taskScheduler, Func func) { var reportProgressTask = Task.Factory.StartNew(func, CancellationToken.None, TaskCreationOptions.None, taskScheduler); return reportProgressTask; } 

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

当您确定在用户界面线程的上下文中调用时, Dispatcher类很有用,并且当您不太确定时, SynchronizationContext很有用。

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

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

SynchronizationContext是一种使用虚方法的抽象。 使用SynchronizationContext允许您不将实现绑定到特定框架。

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

“当您确定在用户界面线程的上下文中调用时,Dispatcher类很有用” – 您正在使用Dispatcher从非UI线程转到UI线程。 这是BeginInvoke的特性。 如果从UI线程调用它,那么代码只是在UI线程中执行。 没有上下文切换。 如果您不想调用BeginInvoke,在UI线程中,您可以调用Dispatcher.Current.CheckAccess(),如果您不在GUI线程中,则返回false