비동기 람다가있는 병렬 foreach


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;

생성 된 모든 스레드가 사실상 백그라운드 스레드이고 Parallel.ForEach호출이 완료를 기다리지 않기 때문에 카운트가 0 인 문제가 발생합니다 . 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람다 내에서 await 키워드를 사용 하는 루프를 어떻게 구현할 수 있습니까? 가능합니까?

Parallel.ForEach 메서드의 프로토 타입은 Action<T>매개 변수로 사용되지만 비동기 람다를 기다립니다.


1
난 당신이 제거 의미 가정 await에서 await GetData(item)-는대로 컴파일 오류가 발생하는 것처럼 두 번째 코드 블록에.
Josh M.

답변:


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;

더 복잡한 것이 필요하다면 Stephen Toub의 ForEachAsync게시물을 확인하십시오 .


46
아마도 조절 메커니즘이 필요합니다. 이것은 10k 네트워크 요청 등으로 끝날 수있는 항목만큼 많은 작업을 즉시 생성합니다.
usr

10
@usr Stephen Toub의 기사에서 마지막 예제가이 문제를 해결합니다.
svick

@svick 나는 그 마지막 샘플에 대해 당황했다. 나에게 더 많은 작업을 만들기 위해 많은 작업을 일괄 처리하지만 모두 대량으로 시작되는 것으로 보입니다.
Luke Puplett

2
@LukePuplett dop작업을 생성 하고 각 작업은 입력 컬렉션의 일부 하위 집합을 직렬로 처리합니다.
svick 2016 년

4
@Afshin_Zavvar : 결과를 Task.Run보내지 않고 전화 await하면 스레드 풀에 화재 및 잊어 버린 작업이 발생합니다. 그것은 거의 항상 실수입니다.
Stephen Cleary

74

AsyncEnumerator NuGet 패키지ParallelForEachAsync확장 메소드를 사용할 수 있습니다 .

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, 그래, 내 꺼야. 나는이 문제를 반복해서 보았으므로 가능한 가장 간단한 방법으로 문제를 해결하고 다른 사람들도 어려움
Serge Semenov

고마워 .. 그것은 확실히 의미가 있고 큰 시간을 도와주었습니다!
Piotr Kula

2
오타가 있습니다 : maxDegreeOfParallelism>maxDegreeOfParalellism
Shiran Dror

3
올바른 철자는 실제로 maxDegreeOfParallelism이지만 @ShiranDror의 의견에는 무언가가 있습니다. 패키지에 변수 maxDegreeOfParalellism을 실수로 불렀습니다. 따라서 인용 된 코드는 변경할 때까지 컴파일되지 않습니다 ..)
BornToCode

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 루프는 세마 폰을 무기한 대기합니다. 문제를 재현하는 다음과 같은 간단한 코드를 사용해보십시오. 2);
nicolay.anykienko

@ nicolay.anykienko 당신은 # 2에 대해 맞습니다. tasksWithThrottler.RemoveAll (x => x.IsCompleted)를 추가하여 메모리 문제를 해결할 수 있습니다.
askids

1
내 코드에서 시도했지만 maxDegreeOfParallelism이 null이 아니면 코드 교착 상태입니다. 여기에서 재생하는 모든 코드를 볼 수 있습니다 stackoverflow.com/questions/58793118/...
마시모 Savazzi
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.