同步上下文和调度程序之间的区别
本文关键字:区别 之间 调度程序 上下文 同步 | 更新日期: 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
对象的包装器,Post
和Send
方法只是委托给Dispatcher.BeginInvoke
和Dispatcher.Invoke
。
所以,即使您决定使用SynchronizationContext
,我认为您最终还是会在幕后呼叫调度员。
此外,我认为使用SynchronizationContext
有点麻烦,因为您必须将对当前上下文的引用传递给所有需要调用UI的线程。
虽然已经指出了差异,但我真的看不出选择其中一个而不是另一个的原因。因此,也许这将有助于解释SynchronizationContext
对象首先试图解决的问题:
- 它提供了一种将工作单元排队到上下文的方法。请注意,这不是特定于线程的,因此我们避免了线程亲和性的问题
- 每个线程都有一个"当前"上下文,但该上下文可能在线程之间共享,即上下文不一定是唯一的
- 上下文保持未完成异步操作的计数。此计数通常会在捕获/排队时递增/递减,但并不总是如此
因此,要回答您选择哪一个的问题,从上面的标准来看,使用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();
});
});
}
此代码按预期工作,但有以下缺点:
- 该代码绑定到WPF
Application
Dispatcher
对象。这使得单元测试和抽象变得困难 - 需要一个
ManualResetEvent
外部对象来在线程之间进行同步。这应该会立即引发代码气味,因为这现在取决于另一个需要嘲笑的资源 - 难以管理所述相同内核对象的对象生存期
现在,让我们使用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线程做出相应的反应。