Async / Await等同于.ContinueWith和CancellationToken以及TaskScheduler.FromCurrentSynchronizationContext()调度程序

这是这个问题的后续行动。

问题 :使用async / await而不是.ContinueWith()来表达以下内容的简洁方法是什么?:

 var task = Task.Run(() => LongRunningAndMightThrow()); m_cts = new CancellationTokenSource(); CancellationToken ct = m_cts.Token; var uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); Task updateUITask = task.ContinueWith(t => UpdateUI(t), ct, TaskContinuationOptions.None, uiTaskScheduler); 

我主要对UI SynchronizationContext的情况感兴趣(例如对于Winforms)

请注意,该行为具有以下所有期望的行为:

  1. CancellationToken被取消时, updateUITask最终会被取消(即LongRunningAndMightThrow工作可能仍会持续很长时间)。

  2. 在运行UpdateUI lambda之前, 在UI线程上检查ct CancellationToken是否取消(请参阅此答案 )。

  3. task完成或出现故障的某些情况下, updateUITask最终会被取消(因为在执行UpdateUI lambda之前在UI线程上检查了ct CancellationToken。

  4. 在UI线程上检查CancellationTokenUpdateUI lambda的运行之间没有中断。 也就是说,如果在UI线程上取消了CancellationTokenSource ,那么在检查CancellationToken和运行UpdateUI lambda之间没有竞争条件 – 没有什么可以在这两个事件之间触发CancellationToken ,因为UI线程在这两个事件之间没有放弃。

讨论:

  • 将此转移到异步/等待的主要目标之一是使UpdateUI在lambda UpdateUI工作(为了便于阅读/调试)。

  • 上面的#1可以通过Stephen Toub的WithCancellation任务扩展方法来解决。 (你可以在答案中随意使用)。

  • 其他要求似乎难以封装到辅助方法而不将UpdateUI作为lambda传递,因为我在CancellationToken的检查和UpdateUI的执行之间不能有中断(即await )(因为我假设我不能依赖于实现细节) await使用这里提到的 ExecuteSynchronously 。这似乎是斯蒂芬谈到的神秘的Task扩展方法.ConfigureAwait(CancellationToken)非常有用。

  • 我已经发布了我现在最好的答案,但我希望有人会提出更好的答案。

示例Winforms应用程序演示用法:

 public partial class Form1 : Form { CancellationTokenSource m_cts = new CancellationTokenSource(); private void Form1_Load(object sender, EventArgs e) { cancelBtn.Enabled = false; } private void cancelBtn_Click(object sender, EventArgs e) { m_cts.Cancel(); cancelBtn.Enabled = false; doWorkBtn.Enabled = true; } private Task DoWorkAsync() { cancelBtn.Enabled = true; doWorkBtn.Enabled = false; var task = Task.Run(() => LongRunningAndMightThrow()); m_cts = new CancellationTokenSource(); CancellationToken ct = m_cts.Token; var uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); Task updateUITask = task.ContinueWith(t => UpdateUI(t), ct, TaskContinuationOptions.None, uiTaskScheduler); return updateUITask; } private async void doWorkBtn_Click(object sender, EventArgs e) { try { await DoWorkAsync(); MessageBox.Show("Completed"); } catch (OperationCanceledException) { MessageBox.Show("Cancelled"); } catch { MessageBox.Show("Faulted"); } } private void UpdateUI(Task t) { // We *only* get here when the cancel button was *not* clicked. cancelBtn.Enabled = false; doWorkBtn.Enabled = true; // Update the UI based on the results of the task (completed/failed) // ... } private bool LongRunningAndMightThrow() { // Might throw, might complete // ... return true; } } 

Stephen Toub的WithCancellation扩展方法:

 public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) { var tcs = new TaskCompletionSource(); using(cancellationToken.Register(s => ((TaskCompletionSource)s).TrySetResult(true), tcs)) if (task != await Task.WhenAny(task, tcs.Task)) throw new OperationCanceledException(cancellationToken); return await task; } 

相关链接:

  • 与等待继续的ContinueWith(delegate,CancellationToken)的等效
  • http://blogs.msdn.com/b/pfxteam/archive/2012/10/05/how-do-i-cancel-non-cancelable-async-operations.aspx
  • https://stackoverflow.com/a/15673072/495262
  • https://stackoverflow.com/a/17560746/495262

只需一行代码就可以更简单地编写WithCancellation方法:

 public static Task WithCancellation(this Task task, CancellationToken token) { return task.ContinueWith(t => t.GetAwaiter().GetResult(), token); } public static Task WithCancellation(this Task task, CancellationToken token) { return task.ContinueWith(t => t.GetAwaiter().GetResult(), token); } 

至于你想要做的操作,只需使用await而不是ContinueWith就像听起来一样简单; 用await替换ContinueWith 。 尽管如此,大多数小件都可以清理干净。

 m_cts.Cancel(); m_cts = new CancellationTokenSource(); var result = await Task.Run(() => LongRunningAndMightThrow()) .WithCancellation(m_cts.Token); UpdateUI(result); 

变化并不大,但他们在那里。 您[可能]想要在开始新操作时取消之前的操作。 如果该要求不存在,请删除相应的行。 取消逻辑都已经由WithCancellation处理,如果请求取消则不需要显式抛出,因为这已经发生。 没有必要将任务或取消令牌存储为局部变量。 UpdateUI不应该接受Task ,它应该只接受一个布尔值。 在调用UpdateUI之前,应该从任务中解包该值。

以下内容应相同:

 var task = Task.Run(() => LongRunningAndMightThrow()); m_cts = new CancellationTokenSource(); CancellationToken ct = m_cts.Token; try { await task.WithCancellation(ct); } finally { ct.ThrowIfCancellationRequested(); UpdateUI(task); } 

请注意,对于LongRunningAndMightThrow方法出错的情况,需要try/finally ,但是当我们返回到UI线程时,已触发CancellationToken 。 没有它,返回的外部Task将出现故障,在最初的ContinueWith情况下,它将被取消。