ด้านหน้าขนานกับแลมด้าแบบอะซิงโครนัส


140

ฉันต้องการจัดการคอลเลคชันควบคู่กันไป แต่ฉันประสบปัญหาในการใช้งานจึงหวังว่าจะได้รับความช่วยเหลือ

ปัญหาเกิดขึ้นถ้าฉันต้องการเรียกวิธีการที่ทำเครื่องหมาย async ใน 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ลูปที่ใช้คีย์เวิร์ด await ภายในแลมบ์ดาได้อย่างไร เป็นไปได้ไหม?

ต้นแบบของเมธอด ParallelForEach ใช้Action<T>เป็นพารามิเตอร์ แต่ฉันต้องการให้มันรอแลมด้าแบบอะซิงโครนัสของฉัน


1
ฉันคิดว่าคุณตั้งใจที่จะลบออกawaitจากawait GetData(item)ในบล็อกโค้ดที่สองของคุณเนื่องจากจะทำให้เกิดข้อผิดพลาดในการคอมไพล์ตามที่เป็นอยู่
Josh M.

2
การทำรัง
ForEach

คำตอบ:


190

หากคุณต้องการเพียงแค่ความเท่าเทียมแบบธรรมดาคุณสามารถทำได้:

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 ตัวอย่างสุดท้ายในบทความของ Stephen Toub กล่าวถึงเรื่องนั้น
svick

@svick ฉันงงกับตัวอย่างสุดท้ายนั้น สำหรับฉันแล้วดูเหมือนว่ามันจะแบ่งงานเป็นกลุ่มเพื่อสร้างงานให้ฉันมากขึ้น แต่พวกเขาทั้งหมดก็เริ่มต้นกันเป็นจำนวนมาก
Luke Puplett

2
@LukePuplett จะสร้างdopงานและแต่ละงานจะประมวลผลชุดย่อยของคอลเลกชันอินพุตเป็นชุด
svick

4
@Afshin_Zavvar: หากคุณโทรTask.Runโดยไม่ได้awaitรับผลลัพธ์นั่นก็เป็นเพียงการทิ้งงานที่ต้องใช้ไฟและลืมลงในเธรดพูล นั่นคือความผิดพลาดเกือบตลอดเวลา
Stephen Cleary

75

คุณสามารถใช้ParallelForEachAsyncวิธีการขยายจาก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
นี่คือแพ็คเกจของคุณ? ฉันเคยเห็นคุณโพสต์สิ่งนี้ในไม่กี่แห่งในขณะนี้? : D โอ้เดี๋ยวก่อน .. ชื่อของคุณอยู่ในแพ็คเกจ: D +1
Piotr Kula

18
@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 async ที่มีน้ำหนักเบาของฉัน

คุณสมบัติ:

  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 loop จะรอเซมาโฟนไปเรื่อย ๆ เพียงลองใช้รหัสง่ายๆนี้ที่ทำให้เกิดปัญหา: await Enumerable.Range (1, 4) .ForEachAsyncConcurrent (async (i) => {Console.WriteLine (i); throw new Exception ("test except");}, maxDegreeOfParallelism: 2);
nicolay.anykienko

@ nicolay.anykienko คุณพูดถูกเกี่ยวกับ # 2 ปัญหาหน่วยความจำนั้นสามารถแก้ไขได้โดยการเพิ่ม taskWithThrottler.RemoveAll (x => x.IsCompleted);
askids

1
ฉันได้ลองใช้รหัสของฉันแล้วและถ้าฉัน maxDegreeOfParallelism ไม่เป็นโมฆะการหยุดชะงักของรหัส คุณสามารถดูรหัสทั้งหมดที่จะทำซ้ำได้ที่นี่: stackoverflow.com/questions/58793118/…
Massimo Savazzi
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.