异步编程时经常需要向用户显示任务的进度并提供取消任务的功能,本篇就讲解在使用 async 时如何做到这两点。

本篇基本就是 Async in 4.5: Enabling Progress and Cancellation in Async APIs 的一个摘录,英语好的话还是推荐阅读原博文。

.NET 中主要有 3 种异步编程模式,对于新代码最推荐的模式就是基于 Task 的 Task-based asynchronous pattern (TAP) ,关于 TAP 模式的详细内容请参考 Docs。

注:Docs 中把 TAP 模式的实现(Implementing)和使用(Consuming)的示例放到了两个文档中。

image.png

汇报进度

通过实现 IProgress 进行进度汇报。

Async 方法内部使用 Report 方法汇报进度:

  1. public async Task<int> UploadPicturesAsync(List<Image> imageList,
  2. IProgress<int> progress)
  3. {
  4. int totalCount = imageList.Count;
  5. int processCount = await Task.Run<int>(() =>
  6. {
  7. int tempCount = 0;
  8. foreach (var image in imageList)
  9. {
  10. //await the processing and uploading logic here
  11. int processed = await UploadAndProcessAsync(image);
  12. if (progress != null)
  13. {
  14. progress.Report((tempCount * 100 / totalCount));
  15. }
  16. tempCount++;
  17. }
  18. return tempCount;
  19. });
  20. return processCount;
  21. }

根据 Report 的进度刷新前台的进度条控件:

  1. private async void Start_Button_Click(object sender, RoutedEventArgs e)
  2. {
  3. int uploads = await UploadPicturesAsync(GenerateTestImages(),
  4. new Progress<int>(percent => progressBar1.Value = percent));
  5. }

取消任务

取消任务用的是 CancellationToken 结构。

CancellationToken 结构:异步方法如果想支持取消,就在异步方法的签名中增加一个 CancellationToken 参数,然后调用者调用异步方法时传入一个 CancellationToken,即调用者和异步方法共享一个 CancellationToken。

最常见的流程是:

  1. 调用者创建一个 CancellationTokenSource 对象
  2. 调用者调用支持取消的异步方法,并传入 CancellationTokenSource.Token
  3. 调用者使用 CancellationTokenSource.Cancel() 来取消异步方法
  4. 异步方法内确认取消并取消任务,通常使用 CancellationToken.ThrowIfCancellationRequested

修改方法签名以传入 CancellationToken,然后在异步方法中通过 ct.ThrowIfCancellationRequested() 检查任务是否已被取消,若已取消则抛出 OperationCanceledException。

  1. public async Task<int> UploadPicturesAsync(List<Image> imageList,
  2. IProgress<int> progress,
  3. CancellationToken ct)
  4. {
  5. int processCount = await Task.Run<int>(() =>
  6. {
  7. foreach (var image in imageList)
  8. {
  9. //await UploadAndProcessAsync (this is another method in the app)
  10. bool success = await UploadAndProcessAsync(image);
  11. ct.ThrowIfCancellationRequested();
  12. // progress logic here
  13. }
  14. },ct);
  15. return processCount;
  16. }

ThrowIfCancellationRequested() 源码:

  1. public void ThrowIfCancellationRequested()
  2. {
  3. if (!this.IsCancellationRequested)
  4. return;
  5. this.ThrowOperationCanceledException();
  6. }

注:.NET 内置的耗时较长的异步方法基本都有支持 CancellationToken 的重载。如 await Task.Delay(2000, ct)

  • 调用方法时传入 CancellationTokenSource 的 Token
  • 通过调用 Cancel 方法触发取消请求
  • 通过捕获 OperationCanceledException 处理任务的取消 ```csharp private CancellationTokenSource _cts;

private async void Start_Button_Click(object sender, RoutedEventArgs e) { var progressIndicator = new Progress(ReportProgress); _cts = new CancellationTokenSource(); try { int x = await UploadPicturesAsync(GenerateTestImages(), progressIndicator, _cts.Token); } catch (OperationCanceledException ex) { //Do stuff to handle cancellation } }

private void Cancel_Button_Click(object sender, RoutedEventArgs e) { _cts.Cancel(); } ```