【问题标题】:Doesn't await when using ForEachAsync with await inside Action将 ForEachAsync 与 Action 内的 await 一起使用时不等待
【发布时间】:2015-06-29 11:50:08
【问题描述】:

以下应该返回“C”,但它返回“B”

using System.Data.Entity;
//...
var state = "A";
var qry = (from f in db.myTable select f);
await qry.ForEachAsync(async (myRecord) => {
   await DoStuffAsync(myRecord);
   state = "B";
});
state = "C";
return state;

它不等待 DoStuffAsync 完成,state="C" 运行,然后state="B" 稍后执行(因为它内部仍在等待)。

【问题讨论】:

  • 所以我猜它在逻辑上相当于foreach (var x in await qty.ToArrayAsync()) { ... }

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


【解决方案1】:

由于DbSet实现IAsyncEnumerable,考虑使用如下扩展方法:

public async static Task ForEachAsync<T>(this IAsyncEnumerable<T> source, Func<T, Task> action, CancellationToken cancellationToken = default)
{
    if (source == null) return;
    await foreach (T item in source.WithCancellation(cancellationToken).ConfigureAwait(false))
    {
        await action(item);
    }
}

用法:

var qry = (from f in db.myTable select f);
await qry
     .AsAsyncEnumerable()
     .ForEachAsync(async arg =>
     {
         await DoStuffAsync(arg);
     });

【讨论】:

  • 您的实现类似于System.Linq.Async 库的ForEachAwaitAsync 运算符。 Todd 的 solution 恕我直言更有趣,因为它允许以不太可能产生问题的方式并发,同时提高性能。
  • 另外ConfigureAwait(false) 表示action 不会在当前SynchronizationContext 上调用。所以例如如果项目的类型是 WinForms 并且 action 包含 UI 相关的代码,ForEachAsync 方法将失败。
  • @Theodor Zoulias 谢谢你的观点,确实,考虑在 UI 应用程序中使用ConfigureAwait(true)(当你需要同步上下文时),否则你应该总是使用ConfigureAwait(false),(查看@987654337 @回答我看得出来他也用过ConfigureAwait(continueOnCapturedContext: false)
  • @Theodor Zoulias “以一种不太可能产生问题的方式允许并发”解释它可能会有所帮助(对我和其他人)。
  • Todd 的解决方案允许一个项目上的每个action 与获取序列的下一个项目同时发生。这两个并发操作不太可能相互依赖(通过共享需要同步的状态),因为它们是完全不同的操作。将此与同时对两个不同元素执行两个操作进行比较。这更有可能产生问题,因为并发操作是同质的,并且可能依赖于一些非线程安全的共享状态(例如 DBConnection)。
【解决方案2】:

这是因为 ForEachAsync 的实现不等待委托的操作

moveNextTask = enumerator.MoveNextAsync(cancellationToken);
action(current);

https://github.com/mono/entityframework/blob/master/src/EntityFramework/Infrastructure/IDbAsyncEnumerableExtensions.cs#L19

但那是因为,你不能等待一个动作,委托需要是一个返回任务的 Func - 请参阅How do you implement an async action delegate method?

因此,在 Microsoft 提供包含 Func 委托的签名并使用 await 调用它之前,您必须推出自己的扩展方法。我目前正在使用以下内容。

public static async Task ForEachAsync<T>(
    this IQueryable<T> enumerable, Func<T, Task> action, CancellationToken cancellationToken) //Now with Func returning Task
{
    var asyncEnumerable = (IDbAsyncEnumerable<T>)enumerable;
    using (var enumerator = asyncEnumerable.GetAsyncEnumerator())
    {

        if (await enumerator.MoveNextAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false))
        {
            Task<bool> moveNextTask;
            do
            {
                var current = enumerator.Current;
                moveNextTask = enumerator.MoveNextAsync(cancellationToken);
                await action(current); //now with await
            }
            while (await moveNextTask.ConfigureAwait(continueOnCapturedContext: false));
        }
    }
}

这样,您的 OP 中的原始测试代码将按预期工作。

【讨论】:

  • 我不确定您自己的 ForEachAsync 与 Action 版本的搭配效果如何。我只是删除了 using System.Data.Entities;并且有我自己的命名空间。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-01-17
  • 1970-01-01
  • 2022-11-07
  • 1970-01-01
  • 1970-01-01
  • 2017-03-01
相关资源
最近更新 更多