【问题标题】:Running async methods in parallel并行运行异步方法
【发布时间】:2016-12-02 17:28:45
【问题描述】:

我有一个异步方法,GetExpensiveThing(),它执行一些昂贵的 I/O 工作。我就是这样使用它的:

// Serial execution
public async Task<List<Thing>> GetThings()
{
    var first = await GetExpensiveThing();
    var second = await GetExpensiveThing();
    return new List<Thing>() { first, second };
}

但由于这是一种昂贵的方法,我想并行执行这些调用。我原以为移动 awaits 会解决这个问题:

// Serial execution
public async Task<List<Thing>> GetThings()
{
    var first = GetExpensiveThing();
    var second = GetExpensiveThing();
    return new List<Thing>() { await first, await second };
}

这不起作用,所以我将它们包装在一些任务中,这有效:

// Parallel execution
public async Task<List<Thing>> GetThings()
{
    var first = Task.Run(() =>
    {
        return GetExpensiveThing();
    });

    var second = Task.Run(() =>
    {
        return GetExpensiveThing();
    });

    return new List<Thing>() { first.Result, second.Result };
}

我什至尝试在任务中和任务周围使用 await 和 async,但它真的很混乱,我没有运气。

是否有更好的并行运行异步方法,或者任务是一种好方法?

【问题讨论】:

  • @bside 你歪曲了链接的帖子。它正确地指出async 函数中的延续是在捕获的上下文中安排的。但在大多数情况下,该上下文是默认的SynchronizationContext,它将继续调度到线程池中,从而使它们并行运行。即使在 WPF 和 ASP 应用程序中,您也可以使用 ConfigureAwait(false) 解决它。 Task.Run 与 CPU 绑定任务一起使用,您不需要它来并行运行延续。
  • @V0ldek 是的,你是对的。感谢您引起我的注意。

标签: c# async-await task-parallel-library


【解决方案1】:

并行运行异步方法是否更好,或者任务是一种好方法?

是的,“最佳”方法是使用Task.WhenAll 方法。但是,您的第二种方法应该并行运行。我创建了一个.NET Fiddle,这应该有助于阐明一些观点。您的第二种方法实际上应该并行运行。我的小提琴证明了这一点!

考虑以下几点:

public Task<Thing[]> GetThingsAsync()
{
    var first = GetExpensiveThingAsync();
    var second = GetExpensiveThingAsync();

    return Task.WhenAll(first, second);
}

注意

最好使用“Async”后缀,而不是 GetThingsGetExpensiveThing - 我们应该分别使用 GetThingsAsyncGetExpensiveThingAsync - source

【讨论】:

  • await Task.WhenAll 将返回Thing[],因此不需要Result(实际上Result 会包装异常;您应该使用await 或使用await Task.WhenAll 的结果)。
  • 很好,谢谢 - 现在还早,我一直在“等待”我的咖啡。 :)
  • 虽然这通常是正确的方法,但 return new List&lt;Thing&gt;() { await first, await second }; 不是在做什么?如果 OP 说这不起作用,那一定是有别的东西在起作用......
  • 这对我不起作用。这些方法仍在按顺序执行。我似乎只能使用这些任务来实现我想要的。 GetExpensiveThing() 的内部实现,一个异步方法本身,会对此有任何影响吗?
  • @davenewza GetExpensiveThing 的实现是什么样的?
【解决方案2】:

您可以使用Task.WhenAll,它会在所有相关任务完成后返回

检查此问题here 以供参考

【讨论】:

    【解决方案3】:

    如果GetExpensiveThing正确 异步的(意味着它不会同步执行任何 IO 或 CPU 工作),那么调用这两种方法然后等待结果的第二种解决方案应该有效。你也可以使用Task.WhenAll

    但是,如果不是,您可以通过将每个任务发布到线程池并使用 Task.WhenAll 组合器来获得更好的结果,例如:

    public Task<IList<Thing>> GetThings() =>
        Task.WhenAll(Task.Run(() => GetExpensiveThing()), Task.Run(() => GetExpensiveThing()));
    

    (请注意,我将返回类型更改为IList 以完全避免awaits。)

    您应该避免使用Result 属性。它会导致调用者线程阻塞并等待任务完成,这与使用延续的awaitTask.WhenAll 不同。

    【讨论】:

      【解决方案4】:

      Task.WhenAll() 倾向于在同时触发大规模/大量任务时表现不佳 - 没有节制/节流。

      如果您正在执行列表中的大量任务并希望等待最终结果,那么我建议使用 partition 并限制并行度。

      我已经修改了Stephen Toub's blog 优雅的现代 LINQ 方法:

      public static Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, Task> funcBody, int maxDoP = 4)
      {
          async Task AwaitPartition(IEnumerator<T> partition)
          {
              using (partition)
              {
                  while (partition.MoveNext())
                  {
                       await Task.Yield(); // prevents a sync/hot thread hangup
                       await funcBody(partition.Current);
                  }
              }
          }
      
          return Task.WhenAll(
              Partitioner
                  .Create(source)
                  .GetPartitions(maxDoP)
                  .AsParallel()
                  .Select(p => AwaitPartition(p)));
      }
      

      它的工作原理很简单,取一个 IEnumerable - 将其分解为均匀的分区,并同时针对每个分区中的每个元素触发一个函数/方法。任何时候每个分区中不超过一个元素,但 n 个分区中有 n 个任务。

      扩展用法:

      await myList.ParallelForEachAsync(myFunc, Environment.ProcessorCount);

      编辑: 如果您需要更多选项,我现在在 Github 上的 repository 中保留一些重载。对于 NetStandard,它也在 NuGet 中。

      编辑 2:感谢下面 Theodor 的 cmets,我能够通过使用 await Task.Yield(); 来缓解编写不佳的异步任务阻塞并行性。

      【讨论】:

      • 不错!如果您想使用 Stephen Toub 的方法但更喜欢使用方法语法 LINQ,这非常完美。
      • 我很高兴你喜欢它,这正是我想要的:)
      • AsParallel 是多余的。后面的Select 不执行 CPU 密集型工作。此外,通过从 Stephen Toub 的代码中删除 Task.Run,如果方法 funcBody 是 CPU 密集型并同步完成,则可能会降低并行度。
      • AsParallel 负责 Task.Run 和 Select 组合 @TheodorZoulias
      • @DanHunex 将Task.Run 替换为AsParallel 会人为地限制并行度,因为缺少配置WithDegreeOfParallelism,因此使用默认的Environment.ProcessorCount。看看this fiddle,其中不尊重请求的maxDoP: 10。然后注释.ParallelForEachAsync_HouseCat 行并取消注释//.ParallelForEachAsync_StephenToub 行,看看现在请求的DOP 得到尊重。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-04-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-05-13
      相关资源
      最近更新 更多