Parallel.ForEach'ta yuvalama bekleniyor


183

Bir metro uygulamasında, bir dizi WCF çağrısı yürütmem gerekiyor. Yapılması gereken önemli sayıda arama var, bu yüzden bunları paralel bir döngüde yapmam gerekiyor. Sorun, WCF çağrıları tamamlanmadan önce paralel döngü çıkmasıdır.

Beklendiği gibi çalışması için bunu nasıl yeniden düzenlersiniz?

var ids = new List<string>() { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };
var customers = new  System.Collections.Concurrent.BlockingCollection<Customer>();

Parallel.ForEach(ids, async i =>
{
    ICustomerRepo repo = new CustomerRepo();
    var cust = await repo.GetCustomer(i);
    customers.Add(cust);
});

foreach ( var customer in customers )
{
    Console.WriteLine(customer.ID);
}

Console.ReadKey();

Yanıtlar:


172

Arkasındaki tüm fikir Parallel.ForEach(), bir dizi iş parçacığına sahip olmanız ve her iş parçacığının koleksiyonun bir bölümünü işlemesidir. Fark ettiğiniz gibi, bu, zaman uyumsuz çağrı süresi boyunca iş parçacığını serbest bırakmak istediğiniz async- ile çalışmaz await.

Bunu, ForEach()iplikleri engelleyerek “düzeltebilirsiniz” , ancak bu async- ' nun bütün noktasını yener await.

Yapabileceğiniz şey , asenkronleri iyi destekleyen TPL Dataflow'u kullanmaktır .Parallel.ForEach()Task

Özellikle, kodunuz TransformBlockher kimliği lambda Customerkullanarak bir dönüştüren bir kullanılarak yazılabilir async. Bu blok paralel çalışacak şekilde yapılandırılabilir. Bu bloğu ActionBlock, her Customerbirini konsola yazan bir bloğa bağlarsınız. Engelleme ağını kurduktan sonra, Post()her kimliğiTransformBlock .

Kodda:

var ids = new List<string> { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };

var getCustomerBlock = new TransformBlock<string, Customer>(
    async i =>
    {
        ICustomerRepo repo = new CustomerRepo();
        return await repo.GetCustomer(i);
    }, new ExecutionDataflowBlockOptions
    {
        MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded
    });
var writeCustomerBlock = new ActionBlock<Customer>(c => Console.WriteLine(c.ID));
getCustomerBlock.LinkTo(
    writeCustomerBlock, new DataflowLinkOptions
    {
        PropagateCompletion = true
    });

foreach (var id in ids)
    getCustomerBlock.Post(id);

getCustomerBlock.Complete();
writeCustomerBlock.Completion.Wait();

Muhtemelen paralelliğini TransformBlockbazı küçük sabitlerle sınırlamak isteseniz de . Ayrıca, örneğin koleksiyon çok büyükse, öğesinin kapasitesini sınırlayabilir TransformBlockve öğeleri eşzamansız olarak ekleyebilirsiniz SendAsync().

Kodunuzla karşılaştırıldığında (işe yaradıysa) ek bir avantaj olarak, yazı tek bir öğe biter bitmez başlayacak ve tüm işlemler bitinceye kadar beklemeyecektir.


2
Kendim gibi netliğe ihtiyaç duyanlar için zaman uyumsuz, reaktif uzantılar, TPL ve TPL DataFlow hakkında kısa bir genel bakış - vantsuyoshi.wordpress.com/2012/01/05/… .
Norman H

1
Eminim bu cevap işleme paralel değildir. Kimlikler üzerinde bir Parallel.ForEach yapmanız ve bunları getCustomerBlock'a göndermeniz gerektiğine inanıyorum. En azından bu öneriyi test ettiğimde bulduğum şey bu.
JasonLind

4
@JasonLind Gerçekten öyle. Paralel Parallel.ForEach()olarak Post()öğeleri kullanmanın gerçek bir etkisi olmamalıdır.
svick

1
@svick Tamam buldum, ActionBlock'un Paralel olması gerekiyor. Bunu biraz farklı yapıyordum, bir dönüşüme ihtiyacım yoktu, bu yüzden sadece bufferblock kullandım ve ActionBlock'ta işimi yaptım. İnterweb'lerle ilgili başka bir cevaptan kafam karıştı.
15:15, JasonLind

2
Bununla, örneğin, TransformBlock'da yaptığınız gibi ActionBlock'ta MaxDegreeOfParallelism'i belirtmek istedim
JasonLind

126

svick cevabı (her zamanki gibi) mükemmel.

Ancak, aktarılacak büyük miktarda veriye sahip olduğunuzda Dataflow'un daha yararlı olduğunu düşünüyorum. Veya asyncuyumlu bir kuyruğa ihtiyacınız olduğunda .

Sizin durumunuzda, daha basit bir çözüm sadece async-style paralelliğini kullanmaktır :

var ids = new List<string>() { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };

var customerTasks = ids.Select(i =>
  {
    ICustomerRepo repo = new CustomerRepo();
    return repo.GetCustomer(i);
  });
var customers = await Task.WhenAll(customerTasks);

foreach (var customer in customers)
{
  Console.WriteLine(customer.ID);
}

Console.ReadKey();

14
Paralelliği (bu durumda büyük olasılıkla yaptığınız) manuel olarak sınırlamak isterseniz, bu şekilde yapmak daha karmaşık olacaktır.
svick

1
Ancak Dataflow'un oldukça karmaşık olabileceği konusunda haklısınız (örneğin, ile karşılaştırıldığında Parallel.ForEach()). Ama şu anda asynckoleksiyonlarla hemen hemen her işi yapmak için en iyi seçenek olduğunu düşünüyorum .
svick

1
@JamesManning nasıl ParallelOptionsyardımcı olacak? Sadece Parallel.For/ForEach/Invoke, kurulan OP olarak burada hiçbir faydası olmayanlar için geçerlidir.
Ohad Schneider

1
@StephenCleary GetCustomerYöntem bir döndürüyorsa Task<T>, biri Select(async i => { await repo.GetCustomer(i);});mi kullanmalıdır ?
Shyju

5
@batmaci: Parallel.ForEachdesteklemiyor async.
Stephen Cleary

81

DataFlow'u önerilen svick olarak kullanmak aşırıya kaçabilir ve Stephen'ın cevabı, operasyonun eşzamanlılığını kontrol etmek için araçlar sağlamaz. Ancak, bu oldukça basit bir şekilde elde edilebilir:

public static async Task RunWithMaxDegreeOfConcurrency<T>(
     int maxDegreeOfConcurrency, IEnumerable<T> collection, Func<T, Task> taskFactory)
{
    var activeTasks = new List<Task>(maxDegreeOfConcurrency);
    foreach (var task in collection.Select(taskFactory))
    {
        activeTasks.Add(task);
        if (activeTasks.Count == maxDegreeOfConcurrency)
        {
            await Task.WhenAny(activeTasks.ToArray());
            //observe exceptions here
            activeTasks.RemoveAll(t => t.IsCompleted); 
        }
    }
    await Task.WhenAll(activeTasks.ToArray()).ContinueWith(t => 
    {
        //observe exceptions in a manner consistent with the above   
    });
}

ToArray()Çağrıları bir dizi yerine bir listesini kullanarak ve tamamlanan görevleri yerine optimize edilebilir, ama çok çoğu senaryolarda bir fark yapacak şüpheliyim. OP sorusu başına örnek kullanım:

RunWithMaxDegreeOfConcurrency(10, ids, async i =>
{
    ICustomerRepo repo = new CustomerRepo();
    var cust = await repo.GetCustomer(i);
    customers.Add(cust);
});

EDIT üyesi SO kullanıcı ve TPL wiz Eli Arbel beni Stephen Toub'un ilgili bir makalesine yönlendirdi . Her zamanki gibi, uygulaması hem zarif hem de etkilidir:

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).ContinueWith(t => 
                          {
                              //observe exceptions
                          });

        })); 
}

1
@RichardPierre aslında bu aşırı yükleme Partitioner.Createyığın bölümleme kullanır, öğeleri farklı görevlere dinamik olarak sağlar, böylece tanımladığınız senaryo gerçekleşmez. Ayrıca, statik (önceden belirlenmiş) bölümlendirmenin, bazı durumlarda daha az ek yük (özellikle senkronizasyon) nedeniyle daha hızlı olabileceğini unutmayın. Daha fazla bilgi için bkz . Msdn.microsoft.com/en-us/library/dd997411(v=vs.110).aspx .
Ohad Schneider

1
@OhadSchneider // istisnaları gözlemle, bu bir istisna atarsa, arayan kişiye doğru havalanır mı? Örneğin, herhangi bir parçası başarısız olursa numaralandırmanın tamamının işlemeyi / başarısız olmasını istersem?
Terry

3
@Tamamen tarafından (tarafından oluşturulan Task.WhenAll) en üstteki görevi ( an içinde AggregateException) istisna içerecek şekilde arayana kadar kabarcıklar ve sonuç olarak, söz konusu arayan kullanılırsa await, çağrı sitesinde bir istisna atılır. Ancak, Task.WhenAllyine de tüm görevlerin tamamlanmasını bekler ve GetPartitionsişlenecek partition.MoveNextbaşka öğe kalmayıncaya kadar çağrıldığında öğeleri dinamik olarak ayırır . Bu, işlemi durdurmak için kendi mekanizmanızı eklemediğiniz sürece (örn. CancellationToken) Kendi başına gerçekleşmeyeceği anlamına gelir.
Ohad Schneider

1
@gibbocool Hala takip ettiğimden emin değilim. Yorumunuzda belirttiğiniz parametrelerle toplam 7 göreviniz olduğunu varsayalım. Ayrıca birinci grubun ara sıra 5 saniye, üç 1 saniye görev aldığını varsayalım. Yaklaşık bir saniye sonra, 5 saniyelik görev hala yürütülmeye devam ederken, 1 saniyelik üç görev tamamlanacaktır. Bu noktada, geri kalan üç adet 1 saniyelik görev yürütülmeye başlayacaktır (bölümleyici tarafından üç "serbest" iş parçacığına sağlanacaktır).
Ohad Schneider

2
@MichaelFreidgeim daha var current = partition.Currentönce olduğu gibi bir şey yapabilir await bodyve daha sonra currentdevamında kullanabilirsiniz ( ContinueWith(t => { ... }).
Ohad Schneider

43

Sorunun asıl gönderildiği 4 yıl önce mevcut olmayan yeni AsyncEnumerator NuGet Paketi ile çabadan tasarruf edebilirsiniz . Paralellik derecesini kontrol etmenizi sağlar:

using System.Collections.Async;
...

await ids.ParallelForEachAsync(async i =>
{
    ICustomerRepo repo = new CustomerRepo();
    var cust = await repo.GetCustomer(i);
    customers.Add(cust);
},
maxDegreeOfParallelism: 10);

Feragatname: Açık kaynak kodlu ve MIT altında lisanslanan AsyncEnumerator kütüphanesinin yazarıyım ve bu mesajı sadece topluluğa yardım etmek için gönderiyorum.


11
Sergey, kütüphanenin yazarı olduğunuzu açıklamalısınız
Michael Freidgeim

5
tamam, feragatnameyi ekledi. Reklam vermek için herhangi bir fayda istemiyorum, sadece insanlara yardım etmek istiyorum;)
Serge Semenov

Kitaplığınız .NET Core ile uyumlu değil.
Corniel Nobel

2
@CornielNobel, .NET Core ile uyumludur - GitHub'daki kaynak kodu hem .NET Framework hem de .NET Core için bir test kapsamına sahiptir.
Serge Semenov

1
@SergeSemenov Kütüphanenizi bunun için çok kullandım AsyncStreamsve mükemmel olduğunu söylemeliyim. Bu kütüphane yeterince tavsiye edilemez.
WBuck

16

Anahtar kelime kullanımı yerine Parallel.Foreacha içine Task.Run()ve içine sarınawait[yourasyncmethod].Result

(UI iş parçacığını engellememek için Task.Run işlemini yapmanız gerekir)

Bunun gibi bir şey:

var yourForeachTask = Task.Run(() =>
        {
            Parallel.ForEach(ids, i =>
            {
                ICustomerRepo repo = new CustomerRepo();
                var cust = repo.GetCustomer(i).Result;
                customers.Add(cust);
            });
        });
await yourForeachTask;

3
Bunun sorunu ne? Ben de aynen böyle yapardım. Her Parallel.ForEachşey tamamlanana kadar engelleyen paralel işi yapalım ve ardından duyarlı bir kullanıcı arayüzüne sahip olmak için her şeyi bir arka plan iş parçacığına itin. Bununla ilgili bir sorun mu var? Belki bu bir uyku ipliği çok fazla, ama kısa, okunabilir bir kod.
ygoe

@LonelyPixel Benim tek sorun çağırır olmasıdır Task.Runzaman TaskCompletionSourcetercih edilir.
Mart'ta Gusdor

1
@Gusdor Meraklı - neden TaskCompletionSourcetercih edilir?
Deniz Balığı

@ Denizyıldızı Keşke cevap verebilmeyi diliyorum Zor bir gün olmalı: D
Gusdor

Kısa bir güncelleme. Şimdi tam olarak bunu arıyordum, en basit çözümü bulmak için aşağı kaydırdım ve tekrar kendi yorumumu buldum. Tam olarak bu kodu kullandım ve beklendiği gibi çalışıyor. Yalnızca döngü içinde orijinal Async çağrılarının bir Senkronizasyon sürümü olduğunu varsayar. awaitekstra değişken adını kaydetmek için öne hareket ettirilebilir.
ygoe

7

Bu oldukça verimli olmalı ve tüm TPL Dataflow'un çalışmasını sağlamaktan daha kolay olmalıdır:

var customers = await ids.SelectAsync(async i =>
{
    ICustomerRepo repo = new CustomerRepo();
    return await repo.GetCustomer(i);
});

...

public static async Task<IList<TResult>> SelectAsync<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> selector, int maxDegreesOfParallelism = 4)
{
    var results = new List<TResult>();

    var activeTasks = new HashSet<Task<TResult>>();
    foreach (var item in source)
    {
        activeTasks.Add(selector(item));
        if (activeTasks.Count >= maxDegreesOfParallelism)
        {
            var completed = await Task.WhenAny(activeTasks);
            activeTasks.Remove(completed);
            results.Add(completed.Result);
        }
    }

    results.AddRange(await Task.WhenAll(activeTasks));
    return results;
}

Kullanım örneği kullanmamalısınız await: gibi var customers = await ids.SelectAsync(async i => { ... });?
Paccc

5

Partiye biraz geç kaldım ama senkronize bağlamda ama aşağıda belirtildiği gibi paralel olmayan kodunuzu çalıştırmak için GetAwaiter.GetResult () kullanmayı düşünebilirsiniz;

 Parallel.ForEach(ids, i =>
{
    ICustomerRepo repo = new CustomerRepo();
    // Run this in thread which Parallel library occupied.
    var cust = repo.GetCustomer(i).GetAwaiter().GetResult();
    customers.Add(cust);
});

5

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

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

5

Bir grup yardımcı yöntem girdikten sonra, bu basit sözdizimiyle paralel sorgular çalıştırabilirsiniz:

const int DegreeOfParallelism = 10;
IEnumerable<double> result = await Enumerable.Range(0, 1000000)
    .Split(DegreeOfParallelism)
    .SelectManyAsync(async i => await CalculateAsync(i).ConfigureAwait(false))
    .ConfigureAwait(false);

Burada olan: Kaynak koleksiyonunu 10 parçaya ( .Split(DegreeOfParallelism)) böldükten sonra, öğelerini teker teker işleyen 10 görev ( .SelectManyAsync(...)) yürütüyoruz ve bunları tek bir liste halinde birleştiriyoruz.

Bahsetmeye değer daha basit bir yaklaşım var:

double[] result2 = await Enumerable.Range(0, 1000000)
    .Select(async i => await CalculateAsync(i).ConfigureAwait(false))
    .WhenAll()
    .ConfigureAwait(false);

Ancak bir önlem alınması gerekir : çok büyük bir kaynak koleksiyonunuz varsa, Taskhemen her öğe için bir zamanlama yapar ve bu da önemli performans isabetlerine neden olabilir.

Yukarıdaki örneklerde kullanılan genişletme yöntemleri aşağıdaki gibidir:

public static class CollectionExtensions
{
    /// <summary>
    /// Splits collection into number of collections of nearly equal size.
    /// </summary>
    public static IEnumerable<List<T>> Split<T>(this IEnumerable<T> src, int slicesCount)
    {
        if (slicesCount <= 0) throw new ArgumentOutOfRangeException(nameof(slicesCount));

        List<T> source = src.ToList();
        var sourceIndex = 0;
        for (var targetIndex = 0; targetIndex < slicesCount; targetIndex++)
        {
            var list = new List<T>();
            int itemsLeft = source.Count - targetIndex;
            while (slicesCount * list.Count < itemsLeft)
            {
                list.Add(source[sourceIndex++]);
            }

            yield return list;
        }
    }

    /// <summary>
    /// Takes collection of collections, projects those in parallel and merges results.
    /// </summary>
    public static async Task<IEnumerable<TResult>> SelectManyAsync<T, TResult>(
        this IEnumerable<IEnumerable<T>> source,
        Func<T, Task<TResult>> func)
    {
        List<TResult>[] slices = await source
            .Select(async slice => await slice.SelectListAsync(func).ConfigureAwait(false))
            .WhenAll()
            .ConfigureAwait(false);
        return slices.SelectMany(s => s);
    }

    /// <summary>Runs selector and awaits results.</summary>
    public static async Task<List<TResult>> SelectListAsync<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> selector)
    {
        List<TResult> result = new List<TResult>();
        foreach (TSource source1 in source)
        {
            TResult result1 = await selector(source1).ConfigureAwait(false);
            result.Add(result1);
        }
        return result;
    }

    /// <summary>Wraps tasks with Task.WhenAll.</summary>
    public static Task<TResult[]> WhenAll<TResult>(this IEnumerable<Task<TResult>> source)
    {
        return Task.WhenAll<TResult>(source);
    }
}
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.