Паралельне передбачення з асинхронною лямбда


138

Я хотів би паралельно обробляти колекцію, але у мене виникають проблеми з її реалізацією, тому я сподіваюся на допомогу.

Проблема виникає, якщо я хочу викликати метод, позначений асинхронією в C #, в межах лямбда паралельного циклу. Наприклад:

var bag = new ConcurrentBag<object>();
Parallel.ForEach(myCollection, async item =>
{
  // some pre stuff
  var response = await GetData(item);
  bag.Add(response);
  // some post stuff
}
var count = bag.Count;

Проблема виникає з кількістю 0, тому що всі створені потоки фактично є лише фоновими нитками, і Parallel.ForEachвиклик не чекає завершення. Якщо я видаляю ключове слово async, метод виглядає приблизно так:

var bag = new ConcurrentBag<object>();
Parallel.ForEach(myCollection, item =>
{
  // some pre stuff
  var responseTask = await GetData(item);
  responseTask.Wait();
  var response = responseTask.Result;
  bag.Add(response);
  // some post stuff
}
var count = bag.Count;

Це працює, але він повністю відключає розум, що очікує, і мені доведеться виконати ручну обробку винятків .. (Видалено для стислості).

Як я можу реалізувати Parallel.ForEachцикл, який використовує ключове слово очікування в лямбда? Це можливо?

Прототип методу Parallel.ForEach приймає за Action<T>параметр, але я хочу, щоб він зачекав мого асинхронного лямбда.


1
Я припускаю, що ви мали намір видалити awaitз await GetData(item)другого блоку коду, оскільки це призведе до помилки компіляції як є.
Джош М.

Відповіді:


187

Якщо ви просто хочете простий паралелізм, ви можете зробити це:

var bag = new ConcurrentBag<object>();
var tasks = myCollection.Select(async item =>
{
  // some pre stuff
  var response = await GetData(item);
  bag.Add(response);
  // some post stuff
});
await Task.WhenAll(tasks);
var count = bag.Count;

Якщо вам потрібно що - то більш складне, перевірити Стівена Toub в ForEachAsyncпост .


46
Можливо, потрібен механізм дроселювання. Це негайно створить стільки завдань, скільки є елементів, які можуть закінчитися мережевими запитами 10k тощо.
usr

10
@usr Останній приклад у статті Стівена Туба стосується цього.
svick

@svick Я спантеличив останній зразок. Мені здається, що це просто збирає безліч завдань, щоб створити мені більше завдань, але всі вони розпочинаються масово.
Люк Пуплетт

2
@LukePuplett Він створює dopзавдання, і кожне з них потім обробляє деякий підмножина колекції входів послідовно.
svick

4
@Afshin_Zavvar: Якщо ви зателефонували Task.Runбез awaitотримання результату, це просто викидання роботи з вогнем і забуттям на пул потоків. Це майже завжди помилка.
Стівен Клірі

74

Ви можете використовувати ParallelForEachAsyncметод розширення з пакету AsyncEnumerator NuGet :

using Dasync.Collections;

var bag = new ConcurrentBag<object>();
await myCollection.ParallelForEachAsync(async item =>
{
  // some pre stuff
  var response = await GetData(item);
  bag.Add(response);
  // some post stuff
}, maxDegreeOfParallelism: 10);
var count = bag.Count;

1
Це ваш пакет? Я бачив, як ви розміщуєте це в кількох місцях зараз? : D О, зачекайте .. ваше ім'я вказано на пакунку: D +1
Piotr Kula

17
@ppumkin, так, це моє. Я бачив цю проблему знову і знову, тому вирішив вирішити її найпростішим способом і звільнити інших від боротьби :)
Сергій Семенов

Дякую .. це, безумовно, має сенс і допомогло мені великий час!
Пьотр Кула

2
у вас помилка друку: maxDegreeOfParallelism>maxDegreeOfParalellism
Ширан Дрор

3
Правильне написання дійсно є maxDegreeOfParallelism, проте в коментарі @ ShiranDror є щось - у вашому пакеті ви помилково назвали змінну maxDegreeOfParalellism (і тому ваш кодований код не
складеться,

17

З допомогою SemaphoreSlimвас можна досягти контролю над паралелізмом.

var bag = new ConcurrentBag<object>();
var maxParallel = 20;
var throttler = new SemaphoreSlim(initialCount: maxParallel);
var tasks = myCollection.Select(async item =>
{
  try
  {
     await throttler.WaitAsync();
     var response = await GetData(item);
     bag.Add(response);
  }
  finally
  {
     throttler.Release();
  }
});
await Task.WhenAll(tasks);
var count = bag.Count;

3

Моя легка реалізація асинхрології ParallelForEach.

Особливості:

  1. Дроселювання (максимальний ступінь паралелізму).
  2. Обробка винятків (виняток агрегації буде передано після завершення).
  3. Пам'ять ефективна (не потрібно зберігати список завдань).

public static class AsyncEx
{
    public static async Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, Task> asyncAction, int maxDegreeOfParallelism = 10)
    {
        var semaphoreSlim = new SemaphoreSlim(maxDegreeOfParallelism);
        var tcs = new TaskCompletionSource<object>();
        var exceptions = new ConcurrentBag<Exception>();
        bool addingCompleted = false;

        foreach (T item in source)
        {
            await semaphoreSlim.WaitAsync();
            asyncAction(item).ContinueWith(t =>
            {
                semaphoreSlim.Release();

                if (t.Exception != null)
                {
                    exceptions.Add(t.Exception);
                }

                if (Volatile.Read(ref addingCompleted) && semaphoreSlim.CurrentCount == maxDegreeOfParallelism)
                {
                    tcs.SetResult(null);
                }
            });
        }

        Volatile.Write(ref addingCompleted, true);
        await tcs.Task;
        if (exceptions.Count > 0)
        {
            throw new AggregateException(exceptions);
        }
    }
}

Приклад використання:

await Enumerable.Range(1, 10000).ParallelForEachAsync(async (i) =>
{
    var data = await GetData(i);
}, maxDegreeOfParallelism: 100);

2

Я створив для цього метод розширення, який використовує SemaphoreSlim, а також дозволяє встановити максимальну ступінь паралелізму

    /// <summary>
    /// Concurrently Executes async actions for each item of <see cref="IEnumerable<typeparamref name="T"/>
    /// </summary>
    /// <typeparam name="T">Type of IEnumerable</typeparam>
    /// <param name="enumerable">instance of <see cref="IEnumerable<typeparamref name="T"/>"/></param>
    /// <param name="action">an async <see cref="Action" /> to execute</param>
    /// <param name="maxDegreeOfParallelism">Optional, An integer that represents the maximum degree of parallelism,
    /// Must be grater than 0</param>
    /// <returns>A Task representing an async operation</returns>
    /// <exception cref="ArgumentOutOfRangeException">If the maxActionsToRunInParallel is less than 1</exception>
    public static async Task ForEachAsyncConcurrent<T>(
        this IEnumerable<T> enumerable,
        Func<T, Task> action,
        int? maxDegreeOfParallelism = null)
    {
        if (maxDegreeOfParallelism.HasValue)
        {
            using (var semaphoreSlim = new SemaphoreSlim(
                maxDegreeOfParallelism.Value, maxDegreeOfParallelism.Value))
            {
                var tasksWithThrottler = new List<Task>();

                foreach (var item in enumerable)
                {
                    // Increment the number of currently running tasks and wait if they are more than limit.
                    await semaphoreSlim.WaitAsync();

                    tasksWithThrottler.Add(Task.Run(async () =>
                    {
                        await action(item).ContinueWith(res =>
                        {
                            // action is completed, so decrement the number of currently running tasks
                            semaphoreSlim.Release();
                        });
                    }));
                }

                // Wait for all tasks to complete.
                await Task.WhenAll(tasksWithThrottler.ToArray());
            }
        }
        else
        {
            await Task.WhenAll(enumerable.Select(item => action(item)));
        }
    }

Використання зразка:

await enumerable.ForEachAsyncConcurrent(
    async item =>
    {
        await SomeAsyncMethod(item);
    },
    5);

"використання" не допоможе. цикл foreach буде чекати семафон нескінченно. Просто спробуйте цей простий код, який відтворює проблему: очікуйте Enumerable.Range (1, 4). 2);
nicolay.anykienko

@ nicolay.anykienko ви праві щодо №2. Цю проблему пам'яті можна вирішити, додавши задачіWithThrottler.RemoveAll (x => x.IsCompleted);
Асідід

1
Я спробував це в своєму коді, і якщо я maxDegreeOfParallelism не обнуляється, кодові тупики. Тут ви можете побачити весь код для відтворення: stackoverflow.com/questions/58793118 / ...
Massimo Savazzi
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.