การเรียกใช้บริการ async หลายตัวพร้อมกัน


17

ฉันมีบริการ async REST ไม่กี่รายการซึ่งไม่ได้ขึ้นอยู่กับกันและกัน นั่นคือในขณะที่ "รอ" คำตอบจาก Service1 ฉันสามารถโทร Service2, Service3 และอื่น ๆ

ตัวอย่างเช่นอ้างถึงรหัสด้านล่าง:

var service1Response = await HttpService1Async();
var service2Response = await HttpService2Async();

// Use service1Response and service2Response

ตอนนี้service2Responseไม่ได้ขึ้นอยู่กับservice1Responseพวกเขาและสามารถดึงข้อมูลได้อย่างอิสระ ดังนั้นฉันไม่จำเป็นต้องรอการตอบรับจากบริการแรกเพื่อเรียกบริการที่สอง

ฉันไม่คิดว่าฉันสามารถใช้Parallel.ForEachที่นี่ได้เนื่องจากไม่ใช่การทำงานของ CPU bound

ในการเรียกการดำเนินการสองอย่างนี้แบบขนานฉันสามารถใช้งานได้Task.WhenAllไหม ปัญหาหนึ่งที่ฉันเห็นการใช้Task.WhenAllคือมันไม่ส่งคืนผลลัพธ์ ในการรับผลลัพธ์ฉันสามารถโทรtask.ResultหลังจากโทรTask.WhenAllได้เนื่องจากงานทั้งหมดเสร็จสมบูรณ์แล้วและสิ่งที่ฉันต้องใช้ในการตอบกลับคืออะไร?

รหัสตัวอย่าง:

var task1 = HttpService1Async();
var task2 = HttpService2Async();

await Task.WhenAll(task1, task2)

var result1 = task1.Result;
var result2 = task2.Result;

// Use result1 and result2

รหัสนี้ดีกว่ารหัสแรกในแง่ของประสิทธิภาพไหม วิธีอื่นใดที่ฉันสามารถใช้ได้


I do not think I can use Parallel.ForEach here since it is not CPU bound operation- ฉันไม่เห็นเหตุผลที่นั่น การเกิดขึ้นพร้อมกันคือการเกิดขึ้นพร้อมกัน
Robert Harvey

3
@RobertHarvey ฉันเดาว่าความกังวลคือในบริบทนี้Parallel.ForEachจะวางไข่เธรดใหม่ในขณะที่async awaitจะทำทุกอย่างในเธรดเดียว
MetaFight

@ ชุดมันขึ้นอยู่กับว่ามันเหมาะสมสำหรับรหัสของคุณเพื่อบล็อก ตัวอย่างที่สองของคุณจะบล็อกจนกว่าคำตอบทั้งสองจะพร้อม ตัวอย่างแรกของคุณสันนิษฐานว่าจะบล็อกด้วยเหตุผลเมื่อรหัสพยายามใช้การตอบกลับ ( await) ก่อนที่จะพร้อม
MetaFight

มันอาจจะง่ายกว่าที่จะให้คำตอบที่น่าพอใจมากขึ้นถ้าคุณให้ตัวอย่างที่เป็นนามธรรมน้อยกว่าของโค้ดที่ใช้ทั้งการตอบสนองบริการ
MetaFight

@MetaFight ในตัวอย่างที่สองของฉันฉันทำWhenAllก่อนที่ฉันจะทำอย่างไรResultกับความคิดที่ว่ามันทำงานเสร็จทั้งหมดก่อนที่จะเรียกว่า Result ตั้งแต่ Task.Result บล็อกการเรียกเธรดฉันคิดว่าถ้าฉันเรียกมันหลังจากที่งานเสร็จสมบูรณ์จริง ๆ แล้วมันจะส่งกลับผลลัพธ์ทันที ฉันต้องการตรวจสอบความเข้าใจ
Ankit Vijay

คำตอบ:


17

ปัญหาหนึ่งที่ฉันเห็นโดยใช้ Task.WhenAll คือมันไม่ส่งคืนผลลัพธ์

แต่มันไม่ส่งกลับผลลัพธ์ พวกเขาทั้งหมดจะอยู่ในประเภททั่วไปดังนั้นจึงไม่มีประโยชน์เสมอไปที่จะใช้ผลลัพธ์เมื่อคุณต้องการค้นหารายการในอาร์เรย์ที่สอดคล้องกับสิ่งTaskที่คุณต้องการผลลัพธ์และอาจนำไปใช้กับมัน ประเภทที่แท้จริงดังนั้นจึงอาจไม่ใช่วิธีที่ง่ายที่สุด / อ่านง่ายที่สุดในบริบทนี้ แต่เมื่อคุณต้องการได้ผลลัพธ์ทั้งหมดจากทุกงานและประเภททั่วไปเป็นประเภทที่คุณต้องการจะปฏิบัติต่อพวกเขามันยอดเยี่ยมมาก .

ในการเรียกผลลัพธ์ฉันสามารถเรียก task.Result หลังจากการเรียก Task.WhenAll เนื่องจากงานทั้งหมดเสร็จสมบูรณ์แล้วและสิ่งที่ฉันต้องใช้ในการตอบสนองคืออะไร?

ใช่คุณสามารถทำได้ คุณสามารถทำได้await( awaitจะแกะข้อยกเว้นในงานที่ผิดพลาดใด ๆ ในขณะที่Resultจะส่งข้อยกเว้นรวม แต่ไม่เช่นนั้นจะเหมือนกัน)

รหัสนี้ดีกว่ารหัสแรกในแง่ของประสิทธิภาพไหม

จะดำเนินการทั้งสองการดำเนินงานในเวลาเดียวกันมากกว่าหนึ่งและอื่น ๆ ไม่ว่าจะดีขึ้นหรือแย่ลงนั้นขึ้นอยู่กับว่าการดำเนินการพื้นฐานเหล่านั้นคืออะไร หากการดำเนินการพื้นฐานคือ "อ่านไฟล์จากดิสก์" การทำแบบขนานนั้นน่าจะช้ากว่าเนื่องจากมีเพียงหนึ่งหัวดิสก์และสามารถอยู่ในที่เดียวในเวลาใดก็ตาม มันกระโดดไปมาระหว่างสองไฟล์จะช้ากว่าการอ่านไฟล์หนึ่งและอีกไฟล์หนึ่ง ในทางกลับกันหากการดำเนินการ "ดำเนินการตามคำขอเครือข่าย" (ตามที่เป็นอยู่ในกรณีนี้) พวกเขาจะมีแนวโน้มที่จะเร็วขึ้น (อย่างน้อยถึงจำนวนคำขอพร้อมกัน) เนื่องจากคุณสามารถรอการตอบกลับได้ จากคอมพิวเตอร์เครือข่ายอื่น ๆ ที่เร็วพอ ๆ กันเมื่อมีคำขอเครือข่ายอื่น ๆ ที่รอดำเนินการอยู่ ถ้าคุณอยากรู้ว่ามัน '

วิธีอื่นใดที่ฉันสามารถใช้ได้

ถ้ามันไม่สำคัญว่าคุณรู้ทุกข้อยกเว้นโยนหมู่ทั้งหมดของการดำเนินงานที่คุณทำในแบบคู่ขนานมากกว่าแค่เป็นคนแรกที่คุณสามารถเพียงแค่awaitงานโดยไม่WhenAllได้ทั้งหมด สิ่งเดียวที่WhenAllทำให้คุณมีAggregateExceptionข้อยกเว้นทุกข้อจากงานที่มีข้อผิดพลาดทุกอย่างแทนที่จะขว้างเมื่อคุณทำภารกิจที่ผิดพลาดครั้งแรก มันง่ายเหมือน:

var task1 = HttpService1Async();
var task2 = HttpService2Async();

var result1 = await task1;
var result2 = await task2;

มันไม่ได้ทำงานอยู่พร้อม ๆ กันโดยลำพังพร้อมกัน คุณกำลังรอให้แต่ละภารกิจดำเนินการตามลำดับ สมบูรณ์ดีถ้าคุณไม่สนใจรหัสนักแสดง
Rick O'Shea

3
@ RickO'Shea มันเริ่มการทำงานตามลำดับ มันจะเริ่มการทำงานครั้งที่สองหลังจากที่ * เริ่มการทำงานครั้งแรก แต่การเริ่มต้นการทำงานแบบอะซิงโครนัสควรเป็นแบบทันทีทันใด (ถ้าไม่ใช่มันไม่ใช่แบบอะซิงโครนัสจริง ๆ และนั่นเป็นจุดบกพร่องในวิธีการนั้น) หลังจากเริ่มต้นหนึ่งและอีกรายการหนึ่งจะไม่ดำเนินการต่อจนกว่าจะเสร็จสิ้นครั้งแรกหลังจากนั้นเสร็จครั้งที่สอง เนื่องจากไม่มีสิ่งใดรอให้คนแรกจนจบก่อนที่จะเริ่มต้นที่สองไม่มีอะไรหยุดพวกเขาจากการทำงานพร้อมกัน (ซึ่งเป็นเช่นเดียวกับที่พวกเขาทำงานแบบขนาน)
Servy

@Servy ฉันไม่คิดว่าเป็นเรื่องจริง ฉันเพิ่มการบันทึกในการดำเนินการ async สองครั้งซึ่งใช้เวลาประมาณหนึ่งวินาทีในแต่ละครั้ง (ทั้งคู่ทำการโทร HTTP) จากนั้นเรียกพวกเขาตามที่คุณแนะนำและแน่ใจว่า task1 เริ่มต้นและสิ้นสุดแล้ว task2 เริ่มและสิ้นสุด
Matt Frear

@MattFrear แล้ววิธีการไม่ได้อยู่ในความเป็นจริงไม่ตรงกัน มันเป็นแบบซิงโครนัส ตามคำนิยามวิธีอะซิงโครนัสกำลังจะส่งคืนทันทีแทนที่จะกลับมาหลังจากการดำเนินการเสร็จสิ้นจริง
Servy

@Servy ตามคำนิยามการรอคอยจะหมายความว่าคุณรอจนกว่างานอะซิงโครนัสจะเสร็จสิ้นก่อนที่จะดำเนินการในบรรทัดถัดไป ไม่ใช่เหรอ
Matt Frear

0

ต่อไปนี้เป็นวิธีการขยายซึ่งใช้ 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);

-2

คุณสามารถใช้

Parallel.Invoke(() =>
{
    HttpService1Async();
},
() =>
{   
    HttpService2Async();
});

หรือ

Task task1 = Task.Run(() => HttpService1Async());
Task task2 = Task.Run(() => HttpService2Async());

//If you wish, you can wait for a particular task to return here like this:
task1.Wait();

ทำไมต้องลงคะแนน
user1451111
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.