如何使用CancellationToken安全地取消任务并等待Task.WhenAll

我有一个框架,它创建一个CancellationTokenSource,配置CancelAfter,然后调用异步方法并传递令牌。 然后异步方法产生许多任务,将取消令牌传递给每个任务,然后等待任务集合。 这些任务都包含通过轮询IsCancellationRequested来优雅取消的逻辑。

我的问题是,如果我将CancellationToken传递给Task.Run(),则抛出包含TaskCanceledException的AggregateException。 这可以防止任务正常取消。

为了解决这个问题,我无法将CancelationToken传递给Task.Run,​​但是我不确定我会丢失什么。 例如,我喜欢这样的想法,即如果我的任务挂起并且无法执行优雅取消,则此exception将强制关闭。 我以为我可以用两个CancelationTokens来处理这个问题,一个’优雅’而另一个’powershell’。 但是,我不喜欢这个解决方案。

这是一些psudo代码代表我上面描述的..

public async Task Main() { CancellationTokenSource cts = new CancellationTokenSource(); cts.CancelAfter(30000); await this.Run(cts.Token); } public async Task Run(CancellationToken cancelationToken) { HashSet tasks = new HashSet(); foreach (var work in this.GetWorkNotPictured) { // Here is where I could pass the Token, // however If I do I cannot cancel gracefully // My dilemma here is by not passing I lose the ability to force // down the thread (via exception) if // it's hung for whatever reason tasks.Add(Task.Run(() => this.DoWork(work, cancelationToken)) } await Task.WhenAll(tasks); // Clean up regardless of if we canceled this.CleanUpAfterWork(); // It is now safe to throw as we have gracefully canceled cancelationToken.ThrowIfCancellationRequested(); } public static void DoWork(work, cancelationToken) { while (work.IsWorking) { if (cancelationToken.IsCancellationRequested) return // cancel gracefully work.DoNextWork(); } } 

我建议您遵循抛出exception的标准取消模式,而不仅仅是返回:

 public static void DoWork(work, cancellationToken) { while (work.IsWorking) { cancellationToken.ThrowIfCancellationRequested(); work.DoNextWork(); } } 

如果你有清理工作要做,那就是finally目的(或using ,如果你可以重构那样):

 public async Task Run(CancellationToken cancellationToken) { HashSet tasks = new HashSet(); foreach (var work in this.GetWorkNotPictured) { tasks.Add(Task.Run(() => this.DoWork(work, cancellationToken)) } try { await Task.WhenAll(tasks); } finally { this.CleanUpAfterWork(); } } 

除了将CancellationToken传递给执行工作的方法之外,还要将CancellationToken提供给Task.Run 。 执行此操作时, Task.Run可以看到抛出的exception是由给出的CancellationToken引起的,并将Task标记为已取消。

 tasks.Add(Task.Run(() => this.DoWork(work, cancelationToken), cancelationToken)); 

一旦完成此操作,您可以确保在取消令牌时DoWork抛出,而不是通过标记为“已成功完成”来检查IsCancellationRequested以尝试结束。