Як я можу використовувати Async з ForEach?


123

Чи можливо використовувати Async під час використання ForEach? Нижче наведений код, який я намагаюся:

using (DataContext db = new DataLayer.DataContext())
{
    db.Groups.ToList().ForEach(i => async {
        await GetAdminsFromGroup(i.Gid);
    });
}

Я отримую помилку:

Назва "Async" не існує в поточному контексті

Метод, до якого вкладається оператор, встановлений на асинхронізацію.

Відповіді:


180

List<T>.ForEachне грає особливо добре async(як і об'єкти LINQ з тих же причин).

У цьому випадку я рекомендую спроектувати кожен елемент в асинхронну операцію, і ви можете потім (асинхронно) дочекатися їх завершення.

using (DataContext db = new DataLayer.DataContext())
{
    var tasks = db.Groups.ToList().Select(i => GetAdminsFromGroupAsync(i.Gid));
    var results = await Task.WhenAll(tasks);
}

Переваги цього підходу від надання asyncделегату ForEach:

  1. Помилка керування є правильнішою. Винятки з async voidне можна назвати catch; такий підхід поширюватиме винятки на await Task.WhenAllлінії, дозволяючи природним чином обробляти винятки.
  2. Ви знаєте, що завдання є завершеними в кінці цього методу, оскільки це робить await Task.WhenAll. Якщо ви користуєтесь async void, ви не можете легко визначити, коли операції завершені.
  3. Цей підхід має природний синтаксис для отримання результатів. GetAdminsFromGroupAsyncздається, що це операція, яка дає результат (адміністратори), і такий код є більш природним, якщо такі операції можуть повернути свої результати, а не встановлювати значення як побічний ефект.

5
Не те, що це щось змінює, але List.ForEach()не є частиною LINQ.
svick

Чудова пропозиція @StephenCleary і дякую за всі відповіді, які ви дали async. Вони були дуже корисні!
Джастін Гельгерсон

4
@StewartAnderson: Завдання виконуватимуться одночасно. Немає розширення для серійного виконання; просто зробіть foreachз awaitвашим корпусом петлі.
Стівен Клірі

1
@mare: ForEachприймає лише синхронний тип делегата, і немає перевантаження з асинхронним типом делегата. Тож коротка відповідь - «ніхто не написав асинхронний ForEach». Більш довга відповідь полягає в тому, що вам доведеться припустити певну семантику; наприклад, чи слід обробляти елементи по одному (як foreach) або одночасно (як Select)? Якщо один за одним, чи не стали б асинхронні потоки кращим рішенням? Якщо одночасно, чи мають бути результати у порядку оригіналу або у порядку завершення? Чи повинен він провалитися при першій відмові або чекати, поки всі завершаться? І т. Д.
Стівен Клірі

2
@RogerWolf: Так; використання SemaphoreSlimдля придушення асинхронних завдань.
Стівен Клірі

61

Цей маленький метод розширення повинен дати вам безпечну для винятків ітерацію асинхронізації:

public static async Task ForEachAsync<T>(this List<T> list, Func<T, Task> func)
{
    foreach (var value in list)
    {
        await func(value);
    }
}

Оскільки ми змінюємо тип повернення лямбда з voidна Task, винятки поширюватимуться правильно. Це дозволить вам написати щось подібне на практиці:

await db.Groups.ToList().ForEachAsync(async i => {
    await GetAdminsFromGroup(i.Gid);
});

Я вважаю, asyncмає бути ранішеi =>
Тодд

Замість того, щоб чекати ForEachAsyn (), можна також зателефонувати Wait ().
Йонас

Ламбду тут не потрібно чекати.
hazzik

Я хотів би додати підтримку CancellationToken в тому , що , як у відповідь Тодда тут stackoverflow.com/questions/29787098 / ...
Zorkind

По ForEachAsyncсуті, це метод бібліотеки, тому очікуване, мабуть, має бути налаштовано ConfigureAwait(false).
Теодор Зуліяс

9

Проста відповідь - використовувати foreachключове слово замість ForEach()методу List().

using (DataContext db = new DataLayer.DataContext())
{
    foreach(var i in db.Groups)
    {
        await GetAdminsFromGroup(i.Gid);
    }
}

Ти геній
Vick_onrails

8

Ось фактична робоча версія вищевказаних варіантів передбачень асинхронізації з послідовною обробкою:

public static async Task ForEachAsync<T>(this List<T> enumerable, Action<T> action)
{
    foreach (var item in enumerable)
        await Task.Run(() => { action(item); }).ConfigureAwait(false);
}

Ось реалізація:

public async void SequentialAsync()
{
    var list = new List<Action>();

    Action action1 = () => {
        //do stuff 1
    };

    Action action2 = () => {
        //do stuff 2
    };

    list.Add(action1);
    list.Add(action2);

    await list.ForEachAsync();
}

У чому полягає ключова відмінність? .ConfigureAwait(false);який зберігає контекст основного потоку під час асинхронізації послідовної обробки кожного завдання.


6

Починаючи з C# 8.0, ви можете створювати та споживати потоки асинхронно.

    private async void button1_Click(object sender, EventArgs e)
    {
        IAsyncEnumerable<int> enumerable = GenerateSequence();

        await foreach (var i in enumerable)
        {
            Debug.WriteLine(i);
        }
    }

    public static async IAsyncEnumerable<int> GenerateSequence()
    {
        for (int i = 0; i < 20; i++)
        {
            await Task.Delay(100);
            yield return i;
        }
    }

Більше


1
Це має перевагу в тому, що, очікуючи кожного елемента, ви тепер також чекаєте MoveNextна перелік. Це важливо в тих випадках, коли перечислювач не може миттєво отримати наступний елемент і повинен зачекати, коли один стане доступним.
Теодор Зуліяс

3

Додайте цей метод розширення

public static class ForEachAsyncExtension
{
    public static Task ForEachAsync<T>(this IEnumerable<T> source, int dop, Func<T, Task> body)
    {
        return Task.WhenAll(from partition in Partitioner.Create(source).GetPartitions(dop) 
            select Task.Run(async delegate
            {
                using (partition)
                    while (partition.MoveNext())
                        await body(partition.Current).ConfigureAwait(false);
            }));
    }
}

А потім використовуйте так:

Task.Run(async () =>
{
    var s3 = new AmazonS3Client(Config.Instance.Aws.Credentials, Config.Instance.Aws.RegionEndpoint);
    var buckets = await s3.ListBucketsAsync();

    foreach (var s3Bucket in buckets.Buckets)
    {
        if (s3Bucket.BucketName.StartsWith("mybucket-"))
        {
            log.Information("Bucket => {BucketName}", s3Bucket.BucketName);

            ListObjectsResponse objects;
            try
            {
                objects = await s3.ListObjectsAsync(s3Bucket.BucketName);
            }
            catch
            {
                log.Error("Error getting objects. Bucket => {BucketName}", s3Bucket.BucketName);
                continue;
            }

            // ForEachAsync (4 is how many tasks you want to run in parallel)
            await objects.S3Objects.ForEachAsync(4, async s3Object =>
            {
                try
                {
                    log.Information("Bucket => {BucketName} => {Key}", s3Bucket.BucketName, s3Object.Key);
                    await s3.DeleteObjectAsync(s3Bucket.BucketName, s3Object.Key);
                }
                catch
                {
                    log.Error("Error deleting bucket {BucketName} object {Key}", s3Bucket.BucketName, s3Object.Key);
                }
            });

            try
            {
                await s3.DeleteBucketAsync(s3Bucket.BucketName);
            }
            catch
            {
                log.Error("Error deleting bucket {BucketName}", s3Bucket.BucketName);
            }
        }
    }
}).Wait();

2

Проблема полягала в тому, що asyncключове слово повинно з’являтися перед лямбда, а не перед тілом:

db.Groups.ToList().ForEach(async (i) => {
    await GetAdminsFromGroup(i.Gid);
});

35
-1 для непотрібного і тонкого використання async void. Такий підхід має проблеми з обробкою виключень і знанням, коли асинхронні операції завершуються.
Стівен Клірі

Так, я виявив, що це не обробляє винятки належним чином.
Герман Шенфельд
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.