【问题标题】:How do I wait on multiple Tasks that accept a CancellationToken?如何等待多个接受 CancellationToken 的任务?
【发布时间】:2013-11-08 16:08:54
【问题描述】:

我的代码使用异步方法从多个来源加载数据(如下所示)。

public class ThEnvironment
{
    public async Task LoadLookupsAsync(CancellationToken cancellationToken) { ... }
    public async Task LoadFoldersAsync(CancellationToken cancellationToken) { ... }
}

在我的程序的其他地方,我想加载多个环境。

ThEnvironment[] workingEnvironments = ThEnvironment.Manager.GetWorkingEnvironments();
foreach ( ThEnvironment environment in workingEnvironments )
{
    await environment.LoadLookupsAsync(CancellationToken.None);
    await environment.LoadFoldersAsync(CancellationToken.None);
}

我的问题有两个:

  1. 对于任何单一环境,我将如何并行运行两个 environment.Load...Async() 方法并且仍然能够传递 CancellationToken?
  2. 如何并行处理所有环境?

底线:我想为所有环境并行运行所有 Load..Async() 方法,并且仍然能够传递 CancellationToken。


更新

感谢大家的快速解答。我已将代码缩减为以下内容:

Refresh();

var tasks = ThEnvironment.Manager.GetWorkingEnvironments()
    .SelectMany(e => new Task[] { e.LoadLookupsAsync(CancellationToken.None), e.LoadFoldersAsync(CancellationToken.None) });

await Task.WhenAll(tasks.ToArray());

我不太关心捕获所有异常,因为这不是生产应用程序。顶部的 Refresh() 处理了表单的初始绘制问题。

到处都是很棒的东西。

【问题讨论】:

    标签: .net async-await cancellation


    【解决方案1】:

    您可以简单地启动两个Tasks,然后等待它们完成:

    Task lookupsTask = environment.LoadLookupsAsync(cancellationToken);
    Task foldersTask = environment.LoadFoldersAsync(cancellationToken);
    
    await lookupsTask;
    await foldersTask;
    

    相同代码的更高效版本是使用Task.WhenAll()

    await Task.WhenAll(lookupsTask, foldersTask);
    

    如果您想对Tasks 的集合执行此操作,而不仅仅是其中两个,您可以用所有Tasks 和await Task.WhenAll(tasksList) 填充List<Task>

    这种方法的一个问题是,如果发生多个异常,您只会得到第一个异常。如果这对您来说是个问题,您可以使用类似 280Z28 建议的方法。

    【讨论】:

      【解决方案2】:

      使用相同的取消令牌启动两个操作,然后等待它们完成。

      Task lookupsTask = environment.LoadLookupsAsync(cancellationToken);
      Task foldersTask = environment.LoadFoldersAsync(cancellationToken);
      
      await Task.Factory.ContinueWhenAll(
          new[] { lookupsTask, foldersTask },
          TaskExtrasExtensions.PropagateExceptions,
          TaskContinuationOptions.ExecuteSynchronously);
      

      这使用了 Parallel Extensions Extras 示例代码中的 PropagateExceptions 扩展方法,以确保来自加载任务的异常信息不会丢失:

      /// <summary>Propagates any exceptions that occurred on the specified tasks.</summary>
      /// <param name="tasks">The Task instances whose exceptions are to be propagated.</param>
      public static void PropagateExceptions(this Task [] tasks)
      {
          if (tasks == null) throw new ArgumentNullException("tasks");
          if (tasks.Any(t => t == null)) throw new ArgumentException("tasks");
          if (tasks.Any(t => !t.IsCompleted)) throw new InvalidOperationException("A task has not completed.");
          Task.WaitAll(tasks);
      }
      

      【讨论】:

      • 您的建议效果很好,但它似乎在执行这些任务时阻塞了 UI 线程。我在主窗体的 Show() 事件处理程序中调用代码。我之前的代码允许表单完全绘制自身并在执行任务之前锁定控件。在任务执行完成之前,您建议的代码不会完全绘制表单。有没有办法让 UI 线程继续? (顺便感谢您的快速回答)
      • @CodeWarrior 此代码不会阻塞 UI 线程。您确定完全按照建议使用它吗?
      • @svick,我可能误认为它实际上阻塞了 UI 线程。我完全按照建议使用它。它确实允许表单加载得更快一些,但表单在第一个 Show() 事件期间不会完全绘制自己。不过,这并不是什么大不了的事。使用 ContinueWhenAll() 仅仅是为了正确传播异常的原因吗?我使用 await Task.WhenAll() 尝试了代码的变体,它的工作方式似乎相同。
      • @CodeWarrior 是的,在异步代码中跟踪异常可能是一场噩梦,所以我非常小心地将原始异常与异步代码流一起保留。 WhenAll 方法不会那样工作。
      • @280Z28,感谢您展示PropagateExceptions,我学到了一个新技巧。我希望有这个选项,虽然我不确定这是一个大问题,IMO (here's some discussion on that)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-04
      • 1970-01-01
      • 2017-07-08
      • 2016-01-17
      • 1970-01-01
      • 2016-03-16
      相关资源
      最近更新 更多