Asenkron lambda ile paralel foreach


140

Bir koleksiyonu paralel olarak ele almak istiyorum, ancak onu uygulamakta güçlük çekiyorum ve bu nedenle biraz yardım umuyorum.

Paralel döngünün lambda'sı içinde C # 'da zaman uyumsuz olarak işaretlenmiş bir yöntemi çağırmak istersem sorun ortaya çıkar. Örneğin:

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;

Sorun, sayımın 0 olmasıyla ortaya çıkar, çünkü oluşturulan tüm iş parçacıkları yalnızca arka planda iş parçacıklarıdır ve Parallel.ForEachçağrı tamamlanmayı beklemez. Eşzamansız anahtar kelimeyi kaldırırsam, yöntem şöyle görünür:

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;

Çalışıyor, ancak bekleme zekasını tamamen devre dışı bırakıyor ve bazı manuel istisna işlemlerini yapmam gerekiyor .. (Kısaca kaldırıldı).

Parallel.ForEachLambda içinde await anahtar sözcüğünü kullanan bir döngüyü nasıl uygulayabilirim ? Mümkün mü?

Parallel.ForEach yönteminin prototipi bir Action<T>parametre olarak alır , ancak eşzamansız lambda'mı beklemesini istiyorum.


1
Ben kaldırmak anlamına varsayalım awaitden await GetData(item)olduğu gibi bir derleme hatası üretecektir gibi ikinci kod bloğu.
Josh M.

Yanıtlar:


190

Sadece basit bir paralellik istiyorsanız, bunu yapabilirsiniz:

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;

Eğer daha karmaşık bir şey gerekiyorsa, kontrol Stephen Toub en ForEachAsyncyazıyı .


46
Muhtemelen bir kısma mekanizmasına ihtiyaç vardır. Bu, 10k ağ istekleriyle sonuçlanabilecek öğeler olduğu kadar hemen çok sayıda görev oluşturacaktır.
usr

10
@usr Stephen Toub'un makalesindeki son örnek bunu ele alıyor.
svick

@svick O son örnek üzerinde kafa karıştırıyordum. Bana öyle geliyor ki, bana daha fazla görev yaratmak için bir sürü görevi topluyor, ama hepsi toplu olarak başlıyor.
Luke Puplett

2
@LukePuplett dopGörevler oluşturur ve daha sonra her biri giriş koleksiyonunun bazı alt kümelerini seri olarak işler.
svick

4
@Afshin_Zavvar: Eğer sonucu Task.Runalmadan ararsanız await, bu sadece iş parçacığı havuzuna ateş ve unut işini fırlatır. Bu neredeyse her zaman bir hatadır.
Stephen Cleary

75

AsyncEnumerator NuGet PaketindenParallelForEachAsync uzantı yöntemini kullanabilirsiniz: You can use the extension method from AsyncEnumerator NuGet Package :

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
Bu senin paketin mi? Şimdi bunu birkaç yerde yayınladığını gördüm? : D Oh bekle .. adınız paketin üzerinde: D +1
Piotr Kula

18
@ppumkin, evet, benim. Bu sorunu defalarca gördüm, bu yüzden mümkün olan en basit şekilde çözmeye ve başkalarını da mücadeleden kurtarmaya karar verdim :)
Serge Semenov

Teşekkürler .. kesinlikle mantıklı ve bana çok yardımcı oldu!
Piotr Kula

2
bir yazım hatası var: maxDegreeOfParallelism>maxDegreeOfParalellism
Shiran Dror

3
Doğru yazım gerçekten maxDegreeOfParallelism'dir, ancak @ ShiranDror'un yorumunda bir şey var - paketinizde yanlışlıkla maxDegreeOfParalellism değişkenini adlandırdınız (ve bu nedenle, alıntılanan kodunuz siz değiştirene kadar derlenmeyecek ..)
BornToCode

17

İle SemaphoreSlimparalellik kontrolü elde edebilirsiniz.

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 async'in hafif uygulamam.

Özellikleri:

  1. Daraltma (maksimum paralellik derecesi).
  2. İstisna işleme (toplama istisnası tamamlandığında atılacaktır).
  3. Hafıza verimli (görevler listesini kaydetmeye gerek yok).

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);
        }
    }
}

Kullanım örneği:

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

2

Bunun için SemaphoreSlim'i kullanan ve aynı zamanda maksimum paralellik derecesini ayarlamaya izin veren bir uzatma yöntemi oluşturdum.

    /// <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)));
        }
    }

Örnek Kullanım:

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

'kullanmak' yardımcı olmayacaktır. foreach döngüsü sonsuza kadar semafonu bekliyor olacak. Sorunu yeniden üreten şu basit kodu deneyin: Enumerable.Range (1, 4) .ForEachAsyncConcurrent (async (i) => {Console.WriteLine (i); yeni İstisna at ("test istisnası");}, maxDegreeOfParallelism: 2);
nicolay.anykienko

@ nicolay.anykienko # 2 konusunda haklısınız. Bu bellek sorunu, tasksWithThrottler.RemoveAll (x => x.IsCompleted);
askids

1
Bunu kodumda denedim ve eğer maxDegreeOfParallelism kod kilitlenmelerini geçersiz kılmaz. Burada yeniden
üretilecek
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.