使用LINQ ForEach()进行异步等待

本文关键字:异步 等待 LINQ ForEach 使用 | 更新日期: 2023-09-27 17:49:27

我有以下正确使用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);
        });
}

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

使用LINQ ForEach()进行异步等待

No。它不是。此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操作,然后await所有操作一起,但这只有在您的操作可以并发运行时才有可能,这并非总是如此(例如实体框架):

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

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

。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://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.parallel.foreachasynchttps://www.hanselman.com/blog/parallelforeachasync-in-net-6

foreach的初始示例有效地在每次循环迭代之后等待。最后一个示例调用List<T>.ForEach(),它具有Action<T>的含义,即您的async lambda将编译成void委托,而不是标准的Task

有效地,ForEach()方法将一个接一个地调用"迭代",而不必等待每个迭代完成。这也将传播到您的方法,这意味着AddReferenceData()可能会在工作完成之前完成。

所以不,它不是等价的,并且行为完全不同。事实上,假设它是一个EF上下文,它可能会爆炸,因为它可能不会并发地跨多个线程使用。

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

要在方法中写入await,需要将方法标记为async。当你写ForEach方法时,你是在你的labda表达式中写await,这是你从你的方法中调用的不同方法。你需要将这个lambda表达式移动到方法中,并将该方法标记为async,也正如@i3arnon所说,你需要async标记ForEach方法,这是。net框架尚未提供的。所以你需要自己写

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

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

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