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

时间:2014-07-10 08:35:45

标签: c# .net

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

Application.Current.Dispatcher.Invoke(myAction);

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

SynchronizationContext.Current.Post(myAction,null);

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

6 个答案:

答案 0 :(得分:24)

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

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

另一方面,

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

您可以在its MSDN documentation中看到继承自SynchronizationContext的类:WindowsFormsSynchronizationContextDispatcherSynchronizationContext


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

答案 1 :(得分:18)

使用WPF时,SynchronizationContext.Current对象的类型为DispatcherSynchronizationContext,它实际上只是Dispatcher对象和Post和{{的包装器1}}方法只委托给SendDispatcher.BeginInvoke

所以即使你决定使用Dispatcher.Invoke,我认为你最终会在幕后调用调度员。

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

答案 2 :(得分:5)

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

  1. 它提供了一种将工作单元排列到上下文的方法。请注意,这不是特定于线程的,因此我们避免了线程关联的问题。
  2. 每个帖子都有一个"当前"上下文,但该上下文可以跨线程共享,即上下文不一定是唯一的。
  3. 上下文保留了一些未完成的异步操作。此计数通常但不总是在捕获/排队时递增/递减。
  4. 因此,为了回答您选择哪一个的问题,从上面的标准来看,使用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. 代码与WPF 应用程序 调度程序对象绑定。这使得单元测试和抽象难以实现。
    2. 需要外部 ManualResetEvent 对象来在线程之间进行同步。这应该立即引起代码气味,因为这现在取决于需要模拟的另一个资源。
    3. 管理所述相同内核对象的对象生存期的难度。
    4. 现在,让我们使用 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);
              });
          }
      

      注意这一次,我们不需要依赖外部对象来在线程之间进行同步。事实上,我们正在 contexts 之间进行同步。

      现在,即使你没有提出要求,但为了完整起见,还有一种方法可以用抽象的方式完成你想要的东西而不需要 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;
          }
      

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

答案 3 :(得分:1)

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

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

答案 4 :(得分:0)

如果您确定在用户界面线程的上下文中调用Dispatcher类,那么SynchronizationContext类非常有用,Dispatcher在您不太确定时很有用。

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

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

答案 5 :(得分:-2)

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