it-swarm.com.ru

C # асинхронное ожидание с использованием LINQ ForEach ()

У меня есть следующий код, который правильно использует парадигму 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);
    }
}

Каков эквивалентный способ написать это, если вместо использования foreach () я хочу использовать LINQ 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);
        });
}

Я беспокоюсь, что этот метод не имеет асинхронной подписи, только тело. Это правильный эквивалент моего первого блока кода выше? 

13
SamDevx

Нет. Эта ForEach не поддерживает async-await и требует, чтобы ваша лямбда была async void, которая должна only использоваться для обработчиков событий. Его использование запустит все ваши операции async одновременно и не будет ждать их завершения.

Вы можете использовать обычную foreach, как и раньше, но если вам нужен метод расширения, вам нужна специальная версия async.

Вы можете создать его самостоятельно, который перебирает элементы, выполняет операцию async и awaits:

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 все вместе, но это возможно только в том случае, если ваши действия могут выполняться одновременно, что не всегда так (например, Entity Framework):

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

Как было отмечено в комментариях, вы, вероятно, не хотите использовать SaveChangesAsync в foreach для начала. Подготовка ваших изменений и последующее их сохранение, вероятно, будут более эффективными. 

18
i3arnon

Почему бы не использовать метод AddRange ()? 

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

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

Начальный пример с foreach фактически ожидает после каждой итерации цикла . Последний пример вызывает List<T>.ForEach(), которая принимает Action<T>, что означает, что ваша асинхронная лямбда будет компилироваться в делегат void, в отличие от стандартного Task.

По сути, метод ForEach() будет вызывать «итерации» одну за другой, не дожидаясь завершения каждой из них. Это также будет распространяться на ваш метод, означая, что AddReferenceData() может закончиться до того, как работа будет завершена.

Так что нет, это не эквивалентно и ведет себя совершенно по-другому. Фактически, если предположить, что это EF-контекст, он взорвется, поскольку его нельзя одновременно использовать в нескольких потоках. 

Также прочитайте http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx упомянутое Дипу, почему, вероятно, лучше придерживаться foreach.

0
Jacek Gorgoń

Чтобы написать await в методе, ваш метод должен быть помечен как асинхронный Когда вы пишете метод ForEach, вы пишете await внутри выражения labda, которое в целом отличается от метода, который вы вызываете из своего метода. Вам нужно переместить это выражение lamdba в метод и пометить этот метод как async, а также, как сказал @ i3arnon. Вам нужен метод ForEach с пометкой async, который еще не предоставлен .Net Framework. Так что вам нужно написать это самостоятельно.

0
Pramod Jangam

Спасибо всем за ваш отзыв. Если взять часть "save" вне цикла, я полагаю, что следующие 2 метода теперь эквивалентны: один использует foreach(), другой - .ForEach(). Однако, как упоминал Дипу, я прочитаю пост Эрика о том, почему 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);
}
0
SamDevx