เหตุใดฉันจึงควรเลือก 'await Task whenAll' มากกว่าการรอหลายครั้ง


127

ในกรณีที่ฉันไม่สนใจลำดับการทำงานให้เสร็จและเพียงแค่ต้องการให้ทุกอย่างเสร็จสมบูรณ์ฉันควรใช้await Task.WhenAllแทนหลาย ๆ ตัวawaitหรือไม่ เช่นDoWork2ด้านล่างเป็นวิธีที่ต้องการDoWork1(และทำไม?):

using System;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static async Task<string> DoTaskAsync(string name, int timeout)
        {
            var start = DateTime.Now;
            Console.WriteLine("Enter {0}, {1}", name, timeout);
            await Task.Delay(timeout);
            Console.WriteLine("Exit {0}, {1}", name, (DateTime.Now - start).TotalMilliseconds);
            return name;
        }

        static async Task DoWork1()
        {
            var t1 = DoTaskAsync("t1.1", 3000);
            var t2 = DoTaskAsync("t1.2", 2000);
            var t3 = DoTaskAsync("t1.3", 1000);

            await t1; await t2; await t3;

            Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }

        static async Task DoWork2()
        {
            var t1 = DoTaskAsync("t2.1", 3000);
            var t2 = DoTaskAsync("t2.2", 2000);
            var t3 = DoTaskAsync("t2.3", 1000);

            await Task.WhenAll(t1, t2, t3);

            Console.WriteLine("DoWork2 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }


        static void Main(string[] args)
        {
            Task.WhenAll(DoWork1(), DoWork2()).Wait();
        }
    }
}

2
จะเกิดอะไรขึ้นหากคุณไม่รู้ว่าต้องทำควบคู่กันไปกี่งาน? จะเกิดอะไรขึ้นถ้าคุณมีงาน 1,000 รายการที่ต้องรัน อันแรกจะอ่านได้ไม่มากawait t1; await t2; ....; await tn=> อันที่สองเป็นตัวเลือกที่ดีที่สุดเสมอ
cuongle

ความคิดเห็นของคุณมีเหตุผล ฉันแค่พยายามชี้แจงบางอย่างสำหรับตัวเองซึ่งเกี่ยวข้องกับคำถามอื่นที่ฉันเพิ่งตอบไป ในกรณีนั้นมี 3 งาน
AVO

คำตอบ:


114

ใช่ใช้WhenAllเพราะเผยแพร่ข้อผิดพลาดทั้งหมดในครั้งเดียว ด้วยการรอหลายครั้งคุณจะสูญเสียข้อผิดพลาดหากมีการรอคอยก่อนหน้านี้

ข้อแตกต่างที่สำคัญอีกประการหนึ่งคือWhenAllจะรอให้งานทั้งหมดเสร็จสิ้นแม้จะมีความล้มเหลว (งานที่ผิดพลาดหรือถูกยกเลิก) การรอด้วยตนเองตามลำดับจะทำให้เกิดการทำงานพร้อมกันโดยไม่คาดคิดเนื่องจากส่วนของโปรแกรมที่ต้องการรอจะดำเนินต่อไปในช่วงต้น

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


9
“ เพราะมันเผยแพร่ข้อผิดพลาดทั้งหมดในครั้งเดียว” ไม่ใช่ถ้าคุณได้awaitผลลัพธ์
svick

2
สำหรับคำถามเกี่ยวกับวิธีจัดการข้อยกเว้นกับ Task บทความนี้ให้ข้อมูลเชิงลึกที่รวดเร็ว แต่ดีต่อเหตุผลเบื้องหลัง (และมันก็เกิดขึ้นเช่นกันที่จะจดบันทึกประโยชน์ของ WhenAll ในทางตรงกันข้ามกับการรอหลาย ๆ ครั้ง): บล็อก .msdn.com / b / pfxteam / archive / 2011/09/28 / 10217876.aspx
Oskar Lindberg

5
@OskarLindberg OP กำลังเริ่มงานทั้งหมดก่อนที่เขาจะรองานแรก ดังนั้นพวกเขาจึงทำงานพร้อมกัน ขอบคุณสำหรับลิงค์
usr

3
@usr ฉันยังอยากรู้อยากเห็นว่า WhenAll ไม่ได้ทำสิ่งที่ชาญฉลาดเช่นการอนุรักษ์ SynchronizationContext เดียวกันหรือไม่เพื่อผลักดันประโยชน์ของมันเพิ่มเติมนอกเหนือจากความหมาย ฉันไม่พบเอกสารสรุป แต่เมื่อดูที่ IL พบว่ามีการใช้งาน IAsyncStateMachine ที่แตกต่างกันอย่างเห็นได้ชัด ฉันไม่ได้อ่าน IL ทุกอย่าง แต่อย่างน้อยที่สุดก็ดูเหมือนจะสร้างรหัส IL ที่มีประสิทธิภาพมากขึ้น (ไม่ว่าในกรณีใดความจริงเพียงอย่างเดียวที่ผลของ WhenAll สะท้อนให้เห็นถึงสถานะของงานทั้งหมดที่เกี่ยวข้องกับฉันก็มีเหตุผลเพียงพอที่จะชอบในกรณีส่วนใหญ่)
Oskar Lindberg

17
ข้อแตกต่างที่สำคัญอีกประการหนึ่งคือ WhenAll จะรอให้งานทั้งหมดเสร็จสิ้นแม้ว่า t1 หรือ t2 จะมีข้อยกเว้นหรือถูกยกเลิก
Magnus

28

ความเข้าใจของฉันคือเหตุผลหลักที่ชอบTask.WhenAllใช้หลายตัวawaitคือประสิทธิภาพ / งาน "ปั่น": DoWork1วิธีการทำสิ่งนี้:

  • เริ่มต้นด้วยบริบทที่กำหนด
  • บันทึกบริบท
  • รอ t1
  • คืนค่าบริบทเดิม
  • บันทึกบริบท
  • รอ t2
  • คืนค่าบริบทเดิม
  • บันทึกบริบท
  • รอ t3
  • คืนค่าบริบทเดิม

ในทางตรงกันข้ามDoWork2ทำสิ่งนี้:

  • เริ่มต้นด้วยบริบทที่กำหนด
  • บันทึกบริบท
  • รอ t1, t2 และ t3 ทั้งหมด
  • คืนค่าบริบทเดิม

ไม่ว่านี่จะเป็นข้อตกลงที่ใหญ่พอสำหรับกรณีเฉพาะของคุณหรือไม่แน่นอน "ขึ้นอยู่กับบริบท" (ให้อภัยการเล่นสำนวน)


4
ดูเหมือนคุณจะคิดว่าการส่งข้อความถึงบริบทการซิงโครไนซ์นั้นมีราคาแพง มันไม่ได้จริงๆ คุณมีผู้รับมอบสิทธิ์ที่ถูกเพิ่มลงในคิวคิวนั้นจะถูกอ่านและดำเนินการผู้รับมอบสิทธิ์ ค่าโสหุ้ยที่เพิ่มนี้มีค่าน้อยมาก ไม่ใช่อะไร แต่ก็ไม่ใหญ่เช่นกัน ค่าใช้จ่ายของการดำเนินการ async ใด ๆ ก็ตามจะลดค่าใช้จ่ายดังกล่าวในเกือบทุกกรณี
Servy

ตกลงมันเป็นเพียงเหตุผลเดียวที่ฉันคิดว่าจะชอบมากกว่าอย่างอื่น นั่นรวมถึงความคล้ายคลึงกันกับ Task ด้วย WaitAll โดยที่การสลับเธรดเป็นต้นทุนที่สำคัญกว่า
Marcel Popescu

1
@Servy ตามที่ Marcel ชี้ให้เห็นว่าขึ้นอยู่กับจริงๆ หากคุณใช้ a waiting กับงาน db ทั้งหมดตามหลักการและฐานข้อมูลนั้นอยู่บนเครื่องเดียวกันกับอินสแตนซ์ asp.net มีหลายกรณีที่คุณจะรอ db hit ที่อยู่ในดัชนีในหน่วยความจำราคาถูกกว่า กว่าสวิตช์ซิงโครไนซ์และเธรดพูลสับเปลี่ยน อาจมีการชนะโดยรวมที่สำคัญด้วย WhenAll () ในสถานการณ์แบบนั้นดังนั้น ... มันขึ้นอยู่กับจริงๆ
Chris Moschini

3
@ChrisMoschini ไม่มีวิธีใดที่แบบสอบถาม DB แม้ว่าจะกดปุ่ม DB ที่อยู่บนเครื่องเดียวกันกับเซิร์ฟเวอร์ แต่จะเร็วกว่าค่าใช้จ่ายในการเพิ่มผู้รับมอบสิทธิ์สองสามคนในปั๊มข้อความ แบบสอบถามในหน่วยความจำนั้นยังคงค่อนข้างช้ากว่ามาก
Servy

นอกจากนี้โปรดทราบว่าหาก t1 ช้าลงและ t2 และ t3 เร็วขึ้น - อีกอันจะรอกลับมาทันที
David Refaeli

18

วิธีการอะซิงโครนัสถูกนำไปใช้เป็นสถานะเครื่อง เป็นไปได้ที่จะเขียนเมธอดเพื่อไม่ให้คอมไพล์ลงใน state-machine ซึ่งมักเรียกกันว่าวิธี async แบบ fast-track สิ่งเหล่านี้สามารถดำเนินการได้ดังนี้:

public Task DoSomethingAsync()
{
    return DoSomethingElseAsync();
}

เมื่อใช้งานTask.WhenAllเป็นไปได้ที่จะรักษารหัสแทร็กด่วนนี้ในขณะที่ยังคงมั่นใจว่าผู้โทรสามารถรอให้งานทั้งหมดเสร็จสิ้นได้เช่น:

public Task DoSomethingAsync()
{
    var t1 = DoTaskAsync("t2.1", 3000);
    var t2 = DoTaskAsync("t2.2", 2000);
    var t3 = DoTaskAsync("t2.3", 1000);

    return Task.WhenAll(t1, t2, t3);
}

7

(ข้อจำกัดความรับผิดชอบ: คำตอบนี้นำมา / ได้รับแรงบันดาลใจจากหลักสูตร TPL Async ของ Ian Griffiths เรื่องPluralsight )

อีกเหตุผลหนึ่งที่ชอบจัดการเมื่อ All is Exception

สมมติว่าคุณมีบล็อกการลองจับวิธี DoWork ของคุณและสมมติว่าพวกเขาเรียกวิธี DoTask ที่แตกต่างกัน:

static async Task DoWork1() // modified with try-catch
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        await t1; await t2; await t3;

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
    }
    catch (Exception x)
    {
        // ...
    }

}

ในกรณีนี้ถ้าทั้ง 3 งานมีข้อยกเว้นจะถูกจับได้เฉพาะงานแรกเท่านั้น ข้อยกเว้นใด ๆ ในภายหลังจะสูญหายไป กล่าวคือถ้า t2 และ t3 มีข้อยกเว้นจะจับเฉพาะ t2 เท่านั้น ฯลฯ ข้อยกเว้นของงานที่ตามมาจะไม่ถูกสังเกต

โดยที่ใน WhenAll - ถ้างานใดงานหนึ่งหรือทั้งหมดเกิดข้อผิดพลาดงานที่ได้จะมีข้อยกเว้นทั้งหมด คีย์เวิร์ด await ยังคงแสดงข้อยกเว้นแรกอีกครั้ง ดังนั้นข้อยกเว้นอื่น ๆ จึงยังไม่สามารถสังเกตได้อย่างมีประสิทธิภาพ วิธีหนึ่งในการเอาชนะสิ่งนี้คือการเพิ่มความต่อเนื่องที่ว่างเปล่าหลังจากภารกิจ WhenAll และรอคอยที่นั่น ด้วยวิธีนี้หากงานล้มเหลวคุณสมบัติผลลัพธ์จะทำให้เกิดข้อยกเว้นรวมทั้งหมด:

static async Task DoWork2() //modified to catch all exceptions
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        var t = Task.WhenAll(t1, t2, t3);
        await t.ContinueWith(x => { });

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t.Result[0], t.Result[1], t.Result[2]));
    }
    catch (Exception x)
    {
        // ...
    }
}

6

คำตอบอื่น ๆ สำหรับคำถามนี้มีเหตุผลทางเทคนิคว่าทำไมจึงawait Task.WhenAll(t1, t2, t3);เป็นที่ต้องการ คำตอบนี้จะมุ่งเป้าไปที่การมองจากด้านที่นุ่มนวลกว่า (ซึ่ง @usr กล่าวถึง) ในขณะที่ยังคงได้ข้อสรุปเดียวกัน

await Task.WhenAll(t1, t2, t3); เป็นแนวทางที่ใช้ได้ผลมากกว่าเนื่องจากเป็นการประกาศเจตนาและเป็นปรมาณู

ด้วยเหตุawait t1; await t2; await t3;นี้จึงไม่มีอะไรขัดขวางเพื่อนร่วมทีม (หรือแม้กระทั่งตัวคุณเองในอนาคต!) จากการเพิ่มรหัสระหว่างawaitข้อความแต่ละข้อความ แน่นอนว่าคุณได้บีบอัดเป็นบรรทัดเดียวเพื่อให้บรรลุเป้าหมายนั้น แต่ก็ไม่ได้ช่วยแก้ปัญหา นอกจากนี้รูปแบบที่ไม่ดีโดยทั่วไปในการตั้งค่าทีมที่จะรวมคำสั่งหลาย ๆ คำไว้ในบรรทัดรหัสที่กำหนดเนื่องจากอาจทำให้ไฟล์ต้นฉบับยากขึ้นสำหรับสายตามนุษย์ในการสแกน

พูดง่ายๆawait Task.WhenAll(t1, t2, t3);ก็คือดูแลรักษาได้มากขึ้นเนื่องจากมันสื่อสารเจตนาของคุณได้ชัดเจนยิ่งขึ้นและมีความเสี่ยงน้อยกว่าต่อข้อบกพร่องเฉพาะที่อาจมาจากการอัปเดตโค้ดที่มีความหมายดีหรือแม้แต่การผสานผิดพลาด

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