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)
请注意,该行为具有以下所有期望的行为:
-
当
CancellationToken
被取消时,updateUITask
最终会被取消(即LongRunningAndMightThrow
工作可能仍会持续很长时间)。 -
在运行UpdateUI lambda之前, 在UI线程上检查
ct
CancellationToken是否取消(请参阅此答案 )。 -
在
task
完成或出现故障的某些情况下,updateUITask
最终会被取消(因为在执行UpdateUI lambda之前在UI线程上检查了ct
CancellationToken。 -
在UI线程上检查
CancellationToken
和UpdateUI
lambda的运行之间没有中断。 也就是说,如果只在UI线程上取消了CancellationTokenSource
,那么在检查CancellationToken
和运行UpdateUI
lambda之间没有竞争条件 – 没有什么可以在这两个事件之间触发CancellationToken
,因为UI线程在这两个事件之间没有放弃。
讨论:
-
将此转移到异步/等待的主要目标之一是使
UpdateUI
在lambdaUpdateUI
工作(为了便于阅读/调试)。 -
上面的#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
情况下,它将被取消。