घोंसले का इंतजार Parallel.ForEach में


183

मेट्रो ऐप में, मुझे कई डब्ल्यूसीएफ कॉल निष्पादित करने की आवश्यकता है। महत्वपूर्ण संख्या में कॉल किए जाने हैं, इसलिए मुझे उन्हें एक समानांतर लूप में करने की आवश्यकता है। समस्या यह है कि WCF कॉल पूरा होने से पहले समानांतर लूप बाहर निकल जाता है।

उम्मीद के मुताबिक काम करने के लिए आप इसे कैसे रिफ्लेक्टर करेंगे?

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

जवाबों:


171

पीछे पूरा विचार Parallel.ForEach()यह है कि आपके पास थ्रेड्स का एक सेट है और प्रत्येक थ्रेड प्रक्रिया संग्रह का हिस्सा है। जैसा कि आपने देखा, यह साथ काम नहीं करता है async- awaitजहाँ आप async कॉल की अवधि के लिए थ्रेड जारी करना चाहते हैं।

आप ForEach()थ्रेड्स को अवरुद्ध करके "ठीक" कर सकते हैं , लेकिन यह पूरे बिंदु को हरा देता है async- await

आप क्या कर सकते हैं इसके बजाय TPL Dataflow का उपयोग करें Parallel.ForEach(), जो अतुल्यकालिक Tasks का समर्थन करता है ।

विशेष रूप से, आपके कोड को एक का उपयोग करके लिखा जा सकता है जो TransformBlockप्रत्येक आईडी को लैम्बडा के Customerउपयोग से बदल देता है async। इस ब्लॉक को समानांतर में निष्पादित करने के लिए कॉन्फ़िगर किया जा सकता है। आप उस ब्लॉक को लिंक करेंगे aActionBlockCustomer जो कंसोल में प्रत्येक को लिखता है। ब्लॉक नेटवर्क सेट करने के बाद, आप Post()प्रत्येक आईडी को TransformBlock

कोड में:

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

यद्यपि आप शायद की समानता को सीमित करना चाहते हैं TransformBlock कुछ छोटे स्थिरांक । इसके अलावा, आप की क्षमता को सीमित कर सकते TransformBlockहैं और आइटम का उपयोग करके इसे अतुल्यकालिक रूप से जोड़ सकते हैं SendAsync(), उदाहरण के लिए यदि संग्रह बहुत बड़ा है।

आपके कोड की तुलना में एक अतिरिक्त लाभ के रूप में (यदि यह काम किया गया है) यह है कि लेखन किसी एकल आइटम के समाप्त होते ही शुरू हो जाएगा, और तब तक प्रतीक्षा न करें जब तक कि सभी संसाधन समाप्त नहीं हो जाते।


2
Async, प्रतिक्रियाशील एक्सटेंशन, TPL और TPL DataFlow का एक बहुत संक्षिप्त अवलोकन - vantsuyoshi.wordpress.com/2012/01/05/… उन लोगों के लिए जो अपने आप को कुछ स्पष्टता की आवश्यकता हो सकती है।
नॉर्मन एच।

1
मुझे पूरा यकीन है कि यह उत्तर प्रसंस्करण को समानांतर नहीं करता है। मेरा मानना ​​है कि आपको आईडी पर एक Parallel.ForEach करने की जरूरत है और उन्हें getCustomerBlock पर पोस्ट करें। जब मैंने इस सुझाव का परीक्षण किया तो कम से कम मुझे यही मिला।
जेसनलिंड

4
@JasonLind यह वास्तव में करता है। समानांतर में वस्तुओं का उपयोग Parallel.ForEach()करने Post()का कोई वास्तविक प्रभाव नहीं होना चाहिए।
svick

1
@svick ठीक है मैंने पाया, ActionBlock को भी समानांतर में होना चाहिए। मैं इसे थोड़ा अलग तरीके से कर रहा था, मुझे ट्रांसफॉर्मेशन की जरूरत नहीं थी इसलिए मैंने सिर्फ एक बफरब्लॉक का इस्तेमाल किया और एक्शनब्लॉक में अपना काम किया। मैं इंटरव्यू पर एक और जवाब से भ्रमित हो गया।
जेसनलिंड

2
जिस से मेरा मतलब है कि MaxBegreeOfParallelism को ActionBlock पर निर्दिष्ट करना जैसे आप अपने उदाहरण में TransformBlock पर करते हैं
जेसनलाइंड

125

svick का जवाब (हमेशा की तरह) उत्कृष्ट है।

हालाँकि, मुझे लगता है कि जब आप वास्तव में बड़ी मात्रा में डेटा ट्रांसफर करना चाहते हैं तो मुझे डेटाफ़्लो अधिक उपयोगी लगता है। या जब आपको asyncअसंगत कतार की आवश्यकता होती है ।

आपके मामले में, एक सरल समाधान सिर्फ async-स्टाइल समानता का उपयोग करना है:

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

13
यदि आप मैन्युअल रूप से समानता (जो आप इस मामले में सबसे अधिक संभावना है) को सीमित करना चाहते थे, तो इस तरह से करना अधिक जटिल होगा।
svick

1
लेकिन आप सही हैं कि Dataflow काफी जटिल हो सकता है (उदाहरण के लिए जब तुलना की जाती है Parallel.ForEach())। लेकिन मुझे लगता है कि वर्तमान में यह asyncसंग्रह के साथ लगभग किसी भी काम को करने का सबसे अच्छा विकल्प है ।
16

1
@JamesManning कैसे ParallelOptionsमदद करने जा रहा है? यह केवल उस पर लागू होता है Parallel.For/ForEach/Invoke, जो ओपी स्थापित होने से यहाँ कोई फायदा नहीं है।
ओहद श्नाइडर

1
@StephenCleary यदि GetCustomerविधि वापस आ रही है Task<T>, तो क्या एक का उपयोग किया जाना चाहिए Select(async i => { await repo.GetCustomer(i);});?
श्यजु

5
@batmaci: Parallel.ForEachसमर्थन नहीं करता है async
स्टीफन Cleary

79

डेटा फ़्लो का उपयोग करना जैसा कि svick ने सुझाव दिया है कि ओवरकिल हो सकता है, और स्टीफन का जवाब ऑपरेशन की समाप्ती को नियंत्रित करने के लिए साधन प्रदान नहीं करता है। हालाँकि, यह केवल प्राप्त किया जा सकता है:

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()कॉल एक सरणी के बजाय की एक सूची का उपयोग करने और पूर्ण हो चुके कार्यों की जगह द्वारा अनुकूलित किया जा सकता है, लेकिन मुझे शक है यह सबसे स्थितियों में बहुत अधिक अंतर होगा। ओपी के प्रश्न के अनुसार नमूना उपयोग:

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

EDIT फेलो SO उपयोगकर्ता और TPL wiz एली अर्बेल ने मुझे स्टीफन टूब के संबंधित लेख की ओर इशारा किया । हमेशा की तरह, उनका कार्यान्वयन सुरुचिपूर्ण और कुशल दोनों है:

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 वास्तव में Partitioner.Createउपयोग करता है यह चंक विभाजन का उपयोग करता है, जो विभिन्न कार्यों को गतिशील रूप से तत्व प्रदान करता है, इसलिए आपके द्वारा वर्णित परिदृश्य नहीं होगा। यह भी ध्यान दें कि स्थैतिक (पूर्व निर्धारित) विभाजन कुछ मामलों में कम ओवरहेड (विशेष रूप से सिंक्रनाइज़ेशन) के कारण तेज हो सकता है। अधिक जानकारी के लिए देखें: msdn.microsoft.com/en-us/library/dd997411(v=vs.110).aspx
ओहद श्नाइडर

1
@OhadSchneider // में अपवादों को देखें, अगर वह एक अपवाद को फेंकता है, तो क्या यह फोन करने वाले को बबल देगा? उदाहरण के लिए, यदि मैं चाहता / चाहती थी कि सम्पूर्ण प्रसंस्करण बंद हो जाए / असफल हो जाए तो क्या उसका कोई भाग विफल हो गया?
टेरी

3
@ यह कॉल करने वाले को इस अर्थ में बबल देगा कि शीर्ष-सर्वाधिक कार्य (द्वारा बनाया गया Task.WhenAll) में अपवाद होगा (अंदर AggregateException), और परिणामस्वरूप यदि कॉलर का उपयोग किया जाता है await, तो अपवाद को कॉल साइट में फेंक दिया जाएगा। हालांकि, Task.WhenAllअभी भी सभी कार्यों के पूरा होने की प्रतीक्षा करेंगे , और GetPartitionsजब partition.MoveNextतक कोई और तत्व प्रक्रिया के लिए नहीं बचेगा, तब तक गतिशील रूप से तत्वों को आवंटित किया जाएगा । इसका मतलब यह है कि जब तक आप प्रसंस्करण को रोकने के लिए अपना खुद का तंत्र नहीं जोड़ते (जैसे CancellationToken) यह अपने आप नहीं होगा।
ओहद श्नाइडर

1
@gibbocool मुझे अभी भी यकीन नहीं है कि मैं अनुसरण करता हूं। मान लीजिए कि आपकी टिप्पणी में आपके द्वारा निर्दिष्ट मापदंडों के साथ आपके कुल 7 कार्य हैं। इसके अलावा मान लें कि पहला बैच कभी-कभार 5 सेकंड का काम करता है, और तीन 1 सेकंड का। लगभग एक सेकंड के बाद, 5-सेकंड का कार्य अभी भी निष्पादित होगा जबकि तीन 1-सेकंड के कार्य समाप्त हो जाएंगे। इस बिंदु पर शेष तीन 1-सेकंड कार्य निष्पादित करना शुरू कर देंगे (उन्हें विभाजनकर्ता द्वारा तीन "मुक्त" थ्रेड्स में आपूर्ति की जाएगी)।
ओहद श्नाइडर

1
@MichaelFreidgeim आप var current = partition.Currentपहले की तरह कुछ कर सकते हैं await bodyऔर फिर currentनिरंतरता ( ContinueWith(t => { ... }) में उपयोग कर सकते हैं ।
ओहद श्नाइडर

41

आप नए AsyncEnumerator NuGet पैकेज के साथ प्रयास को बचा सकते हैं , जो 4 साल पहले मौजूद नहीं था जब मूल रूप से प्रश्न पोस्ट किया गया था। यह आपको समानता की डिग्री को नियंत्रित करने की अनुमति देता है:

using System.Collections.Async;
...

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

अस्वीकरण: मैं AsyncEnumerator पुस्तकालय का लेखक हूं, जो खुला स्रोत है और MIT के तहत लाइसेंस प्राप्त है, और मैं इस संदेश को सिर्फ समुदाय की मदद करने के लिए पोस्ट कर रहा हूं।


11
सर्गेई, आपको खुलासा करना चाहिए कि आप पुस्तकालय के एक लेखक हैं
माइकल फ्रीजिम

5
ठीक है, अस्वीकरण जोड़ा गया। मैं इसे विज्ञापन से कोई लाभ नहीं चाह रहा हूं, बस लोगों की मदद करना चाहता हूं;)
सर्ज सेमेनोव

आपकी लाइब्रेरी .NET कोर के साथ संगत नहीं है।
कॉर्निएल नोबेल

2
@CornielNobel, यह .NET कोर के साथ संगत है - GitHub के स्रोत कोड में .NET फ्रेमवर्क और .NET कोर दोनों के लिए एक परीक्षण कवरेज है।
सर्ज सेमेनोव

1
@SergeSemenov मैंने आपके पुस्तकालय को इसके लिए बहुत उपयोग किया है AsyncStreamsऔर मुझे यह कहना बहुत अच्छा है। इस पुस्तकालय की पर्याप्त अनुशंसा नहीं कर सकते।
WBuck

16

कीवर्ड उपयोग के बजाय Parallel.Foreachएक में लपेटेंTask.Run()await[yourasyncmethod].Result

(आपको यूआई थ्रेड को ब्लॉक नहीं करने के लिए टास्क करना होगा।

कुछ इस तरह:

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

3
इससे क्या परेशानी है? मैंने इसे बिल्कुल इसी तरह किया होगा। आज्ञा देना Parallel.ForEachसमानांतर काम है, जो सभी जब तक ब्लॉक किया जाता हैं, और तब एक पृष्ठभूमि धागा करने के लिए पूरी बात धक्का एक संवेदनशील यूआई है। उस के साथ कोई समस्या? हो सकता है कि यह एक सोने का धागा है, लेकिन यह कम, पठनीय कोड है।
यज्ञ

@ LonelyPixel मेरा एकमात्र मुद्दा यह है कि यह Task.Runजब TaskCompletionSourceबेहतर हो तब कॉल करता है।
गुस्सोर

1
@Gusdor जिज्ञासु - क्यों TaskCompletionSourceबेहतर है?
सीफिश जूल

@ सीफिश एक अच्छा सवाल है जो मैं चाहता हूं कि मैं जवाब दे सकूं। एक कठिन दिन रहा होगा: D
Gusdor

बस एक छोटी अद्यतन। मैं अब ठीक इसी तलाश में था, सबसे सरल समाधान खोजने के लिए नीचे स्क्रॉल किया और अपनी टिप्पणी फिर से पाया। मैंने इस कोड का बिल्कुल उपयोग किया है और यह उम्मीद के मुताबिक काम करता है। यह केवल मानता है कि लूप के भीतर मूल Async कॉल का एक सिंक संस्करण है। awaitअतिरिक्त चर नाम को सहेजने के लिए सामने की ओर ले जाया जा सकता है।
योगो

7

यह पूरे टीपीएल डाटाफ्लो के काम करने की तुलना में बहुत कुशल और आसान होना चाहिए:

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

उपयोग उदाहरण का उपयोग नहीं करना चाहिए awaitजैसे var customers = await ids.SelectAsync(async i => { ... });:?
पक्का

5

मुझे पार्टी करने में थोड़ी देर हो गई है, लेकिन आप अपने async कोड को सिंक संदर्भ में चलाने के लिए GetAwaiter.GetResult () का उपयोग करने पर विचार करना चाह सकते हैं, लेकिन नीचे के रूप में;

 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

इसके लिए एक विस्तार विधि जो सेमाफोरस्लीम का उपयोग करती है और अधिकतम समानता सेट करने की भी अनुमति देती है

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

5

सहायक विधियों का एक समूह शुरू करने के बाद, आप इस सरल वाक्यविन्यास के साथ समानांतर प्रश्नों को चलाने में सक्षम होंगे:

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

यहां क्या होता है: हम स्रोत संग्रह को 10 विखंडू ( .Split(DegreeOfParallelism)) में विभाजित करते हैं , फिर 10 कार्यों को प्रत्येक आइटम को एक-एक करके संसाधित करते हैं ( .SelectManyAsync(...)) और एक ही सूची में वापस मर्ज करते हैं।

एक सरल दृष्टिकोण का उल्लेख करते हुए कहा गया है:

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

लेकिन इसमें एहतियात बरतने की ज़रूरत है : यदि आपके पास कोई स्रोत संग्रह है जो बहुत बड़ा है, तो यह Taskतुरंत प्रत्येक आइटम के लिए शेड्यूल करेगा , जिससे महत्वपूर्ण प्रदर्शन हिट हो सकते हैं।

ऊपर दिए गए उदाहरणों में उपयोग की जाने वाली विस्तार विधियाँ निम्नानुसार हैं:

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);
    }
}
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.