【问题标题】:Async await using LINQ ForEach()使用 LINQ ForEach() 进行异步等待
【发布时间】:2015-07-27 10:59:44
【问题描述】:

我有以下正确使用 async/await 范例的代码。

internal static async Task AddReferencseData(ConfigurationDbContext context)
{
    foreach (var sinkName in RequiredSinkTypeList)
    {
        var sinkType = new SinkType() { Name = sinkName };
        context.SinkTypeCollection.Add(sinkType);
        await context.SaveChangesAsync().ConfigureAwait(false);
    }
}

如果我想使用 LINQ ForEach() 而不是使用 foreach(),那么写这个的等效方法是什么?例如,这个会给出编译错误。

internal static async Task AddReferenceData(ConfigurationDbContext context)
{
    RequiredSinkTypeList.ForEach(
        sinkName =>
        {
            var sinkType = new SinkType() { Name = sinkName };
            context.SinkTypeCollection.Add(sinkType);
            await context.SaveChangesAsync().ConfigureAwait(false);
        });
}

唯一没有编译错误的代码就是这个。

internal static void AddReferenceData(ConfigurationDbContext context)
{
    RequiredSinkTypeList.ForEach(
        async sinkName =>
        {
            var sinkType = new SinkType() { Name = sinkName };
            context.SinkTypeCollection.Add(sinkType);
            await context.SaveChangesAsync().ConfigureAwait(false);
        });
}

我担心这个方法没有异步签名,只有主体有。 这是我上面的第一个代码块的正确等价物吗?

【问题讨论】:

  • ForEach 不是 linq 函数
  • 你为什么要改变它?如果它没有损坏,请不要修复它。
  • Eric Lipert 使用“foreach”与“ForEach”的优秀帖子blogs.msdn.com/b/ericlippert/archive/2009/05/18/…
  • 如果我打算做任何事情,我会将SaveChangesAsync 放在循环之外。我怀疑把它放在里面有什么好处(相反,可能会有性能损失)。
  • @Charles Mager 确实 - 将 SaveChanges() 置于循环中几乎总是一个坏主意。另一方面,您为什么要准确调用异步版本?

标签: c# .net entity-framework linq async-await


【解决方案1】:

没有。它不是。这个ForEach 不支持async-await 并且要求您的lambda 是async void,它应该 用于事件处理程序。使用它将同时运行您的所有 async 操作,并且不会等待它们完成。

您可以像以前一样使用常规的foreach,但如果您想要一个扩展方法,您需要一个特殊的async 版本来迭代项目,执行async 操作和awaits 它。

但是,您可以创建一个:

从 .NET 6.0 开始,您可以使用 Parallel.ForEachAsync:

public async Task ForEachAsync<T>(this IEnumerable<T> enumerable, Func<T, Task> action)
{
    await Parallel.ForEachAsync(
        enumerable,
        async (item, _) => await action(item));
}

或者,避免扩展方法,直接调用它:

await Parallel.ForEachAsync(
    RequiredSinkTypeList,
    async (sinkName, _) =>
    {
        var sinkType = new SinkType() { Name = sinkName };
        context.SinkTypeCollection.Add(sinkType);
        await context.SaveChangesAsync().ConfigureAwait(false);
    });

在旧平台上你需要使用foreach:

public async Task ForEachAsync<T>(this IEnumerable<T> enumerable, Func<T, Task> action)
{
    foreach (var item in enumerable)
    {
        await action(item);
    }
}

用法:

internal static async Task AddReferencseData(ConfigurationDbContext context)
{
    await RequiredSinkTypeList.ForEachAsync(async sinkName =>
    {
        var sinkType = new SinkType() { Name = sinkName };
        context.SinkTypeCollection.Add(sinkType);
        await context.SaveChangesAsync().ConfigureAwait(false);
    });
}

ForEachAsync 的不同(通常更有效)实现是启动所有 async 操作,然后才将所有 await 一起启动,但只有当您的操作可以同时运行时才有可能总是这样(例如实体框架):

public Task ForEachAsync<T>(this IEnumerable<T> enumerable, Func<T, Task> action)
{
    return Task.WhenAll(enumerable.Select(item => action(item)));
}

正如 cmets 中所指出的,您可能不想在 foreach 开始时使用 SaveChangesAsync。准备您的更改,然后一次保存它们可能会更有效。

【讨论】:

  • @i3amon 谢谢我喜欢你最后的建议。如果“SinkTypeCollection”是线程安全类型,那会起作用吗?
  • @SamDevx 实际上,如果它不在异步 lambda 的同步部分中,它也可以工作。这个问题真的在SaveChangesAsync
【解决方案2】:

.Net 6 现在有

 Parallel.ForEachAsync

习惯了

await Parallel.ForEachAsync(userHandlers, parallelOptions, async (uri, token) =>
{
    var user = await client.GetFromJsonAsync<GitHubUser>(uri, token);
 
    Console.WriteLine($"Name: {user.Name}\nBio: {user.Bio}");
});

https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.parallel.foreachasync https://www.hanselman.com/blog/parallelforeachasync-in-net-6

【讨论】:

    【解决方案3】:

    要在方法中写入 await,您的方法需要标记为异步。 当您编写 ForEach 方法时,您正在您的 labda 表达式中编写 await,这完全是您从方法中调用的不同方法。您需要将此 lamdba 表达式移动到方法并将该方法标记为异步,并且正如@i3arnon 所说,您需要.Net Framework 尚未提供的异步标记的 ForEach 方法。所以需要自己写。

    【讨论】:

      【解决方案4】:

      foreach 的初始示例在每次循环迭代后有效地等待。 最后一个示例调用List&lt;T&gt;.ForEach(),它采用Action&lt;T&gt; 的含义,即您的异步lambda 将编译为void 委托,而不是标准的Task

      实际上,ForEach() 方法将逐个调用“迭代”,而无需等待每个迭代完成。这也会传播到您的方法,这意味着AddReferenceData() 可能会在工作完成之前完成。

      所以不,它不是等效的并且行为完全不同。事实上,假设它是一个 EF 上下文,它会崩溃,因为它可能不会同时在多个线程中使用。

      另请阅读 Deepu 提到的http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx,了解为什么坚持使用foreach 可能更好。

      【讨论】:

        【解决方案5】:

        为什么不使用 AddRange() 方法?

        context.SinkTypeCollection.AddRange(RequiredSinkTypeList.Select( sinkName  => new SinkType() { Name = sinkName } );
        
        await context.SaveChangesAsync().ConfigureAwait(false);
        

        【讨论】:

          【解决方案6】:

          感谢大家的反馈。将 "save" 部分放在循环之外,我相信以下两种方法现在是等效的,一种使用foreach(),另一种使用.ForEach()。不过,正如 Deepu 所说,我会阅读 Eric 的帖子,了解为什么 foreach 可能会更好。

          public static async Task AddReferencseData(ConfigurationDbContext context)
          {
              RequiredSinkTypeList.ForEach(
                  sinkName =>
                  {
                      var sinkType = new SinkType() { Name = sinkName };
                      context.SinkTypeCollection.Add(sinkType);
                  });
              await context.SaveChangesAsync().ConfigureAwait(false);
          }
          
          public static async Task AddReferenceData(ConfigurationDbContext context)
          {
              foreach (var sinkName in RequiredSinkTypeList)
              {
                  var sinkType = new SinkType() { Name = sinkName };
                  context.SinkTypeCollection.Add(sinkType);
              }
              await context.SaveChangesAsync().ConfigureAwait(false);
          }
          

          【讨论】:

            猜你喜欢
            • 2023-03-30
            • 1970-01-01
            • 2019-02-10
            • 2019-07-29
            • 2021-08-13
            • 2013-08-01
            • 1970-01-01
            • 2017-05-11
            相关资源
            最近更新 更多