异步等待使用LINQ的ForEach()

36

我有如下代码,它正确地使用了异步/等待编程范式。

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(),那么写这个if语句的等效方法是什么?例如,这个会导致编译错误。

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);
        });
}

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


9
ForEach 不是LINQ函数。 - Grundy
2
为什么你想要改变它呢?如果它没有出现问题,就不要去修复它。 - H H
1
Eric Lipert的博客文章非常棒,讨论了“foreach”和“ForEach”的区别。http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx - Deepu Madhusoodanan
3
如果我要做任何事情,我会把SaveChangesAsync放在循环外部。我怀疑将其放在内部没有任何优势(相反,可能会有性能惩罚)。 - Charles Mager
1
@Charles Mager确实 - 在循环中放置“SaveChanges()”几乎总是一个坏主意。另一方面,你为什么要精确调用异步版本呢? - Jacek Gorgoń
1
对 Eric Lipert 博客的引用似乎不再有效。这是他网站上应该有相同文章的有效链接:https://ericlippert.com/2009/05/18/foreach-vs-foreach/ - Bob Gear
6个回答

49

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

您可以像以前一样使用普通的foreach,但如果您想要一个扩展方法,您需要一个特殊的async版本,它遍历项目,执行async操作并await它。

您可以创建一个:

从.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 操作,然后一起等待所有操作完成,但这仅在您的操作可以并发运行时才可能实现(例如,Entity Framework):

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

正如评论中所指出的,您可能不希望在foreach中使用SaveChangesAsync。准备好您的更改,然后一次性保存它们可能会更有效率。


@i3amon 谢谢,我喜欢你的最后一个建议。如果 "SinkTypeCollection" 是线程安全类型,那么这个方案可行吗? - SamDevx
@SamDevx 实际上,如果它不在异步 Lambda 的同步部分中,它也可以工作。问题实际上在于 SaveChangesAsync - i3arnon

3

1

foreach的初始示例在每次循环迭代后都会有效等待。

最后一个示例调用了带有Action<T>参数的List<T>.ForEach(),意味着您的异步lambda将编译为void委托,而不是标准的Task

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

因此,它们并不相等且表现行为非常不同。实际上,假设这是一个EF上下文,则可能会崩溃,因为它不能跨多个线程同时使用。

还要阅读Deepu提到的http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx以了解为什么最好坚持使用foreach


0
要在方法中编写await,您的方法需要标记为async。 当您编写ForEach方法时,您正在编写在其中调用不同方法的lambda表达式中的await。 您需要将此lambda表达式移动到方法中,并将该方法标记为async,正如@i3arnon所说,您需要标记为async的ForEach方法,但.Net Framework尚未提供。 因此,您需要自己编写它。

0

感谢大家的反馈。将"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);
}

-1
为什么不使用AddRange()方法?
context.SinkTypeCollection.AddRange(RequiredSinkTypeList.Select( sinkName  => new SinkType() { Name = sinkName } );

await context.SaveChangesAsync().ConfigureAwait(false);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接