假设我有一个尝试处理多线程环境的Form
;因此,在完成任何UI修改之前,它会检查它是否在UI线程上运行:
partial class SomeForm : Form
{
public void DoSomethingToUserInterface()
{
if (InvokeRequired)
{
BeginInvoke(delegate { DoSomethingToUserInterface() });
}
else
{
… // do the actual work (e.g. manipulate the form or its elements)
}
}
}
现在假设我在该方法的…
部分中执行一些冗长的操作;因此,我想使用 async/await
使其异步。
鉴于我应该更改方法签名以返回Task
而不是void
(以便可以捕获异常),我将如何实现执行BeginInvoke
的部分?它应该返回什么?
public async Task DoSomethingToUserInterfaceAsync()
{
if (InvokeRequired)
{
// what do I put here?
}
{
… // like before (but can now use `await` expressions)
}
}
将async-await
与自定义awaiter
(如通用TaskAwaiter
)一起使用时,将为您隐式捕获SynchronizationContext
。稍后,当异步方法完成时,将使用 SynchronizationContext.Post
将延续(await
之后的任何代码)封送回相同的同步上下文。
这完全消除了使用InvokeRequired
和其他技术来维护 UI 线程工作的需要。为此,您必须将方法调用一直跟踪到顶级调用,并重构它们以可能使用async-await
。
但是,要按原样解决特定问题,您可以做的是在Form
初始化时捕获WinFormSynchronizationContext
:
partial class SomeForm : Form
{
private TaskScheduler _uiTaskScheduler;
public SomeForm()
{
_uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
}
}
稍后当您要await
时使用它:
if (InvokeRequired)
{
Task uiTask = new Task(() => DoSomethingToUserInterface());
uiTask.RunSynchronously(_uiTaskScheduler);
}
else
{
// Do async work
}
您可以使用 TaskScheduler.FromCurrentSynchronizationContext 获取当前同步上下文的任务计划程序(对于 UI 线程),并将其存储在字段中供以后使用。
然后,当您有兴趣在 UI 线程中启动任何任务时,您必须将uiScheduler
传递给 StartNew 方法,以便 TPL 将在提供的计划程序(在本例中为 UI 线程)中计划任务。
无论如何,您决定在UI线程中运行这些东西,因此只需将其安排到UIScheduler,您无需检查InvokeRequired
。
public async Task DoSomethingToUserInterfaceAsync()
{
await Task.Factory.StartNew(() => DoSomethingToUserInterface(), CancellationToken.None, TaskCreationOptions.None, uiScheduler);
...
}
若要检索 UI 计划程序,可以使用以下代码
private TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
注意:TaskScheduler.FromCurrentSynchronizationContext
应该只从 UI 线程调用,这一点非常重要,否则,它将引发异常,或者你会得到一些其他SynchronizationContext
的TaskScheduler
,这些不会满足你的需要。
另请注意,如果您已从 UI 线程本身启动异步操作,则不需要上述任何魔法。 await 将在已启动的上下文中恢复。