ความแตกต่างระหว่าง await และ ContinueWith


119

คนที่สามารถอธิบายถ้าawaitและContinueWithมีความหมายเหมือนหรือไม่ได้อยู่ในตัวอย่างต่อไปนี้ ฉันพยายามใช้ TPL เป็นครั้งแรกและอ่านเอกสารทั้งหมดแล้ว แต่ไม่เข้าใจความแตกต่าง

รอ :

String webText = await getWebPage(uri);
await parseData(webText);

ดำเนินการต่อด้วย :

Task<String> webText = new Task<String>(() => getWebPage(uri));
Task continue = webText.ContinueWith((task) =>  parseData(task.Result));
webText.Start();
continue.Wait();

เป็นที่ต้องการมากกว่าอีกสถานการณ์หนึ่งหรือไม่?


3
หากคุณลบWaitการโทรในตัวอย่างที่สองแล้วทั้งสองจะเป็นตัวอย่าง (ส่วนใหญ่) เทียบเท่า
Servy


FYI: getWebPageวิธีของคุณไม่สามารถใช้กับทั้งสองรหัสได้ ในรหัสแรกมีTask<string>ประเภทการส่งคืนในขณะที่ในรหัสที่สองมีstringประเภทการส่งคืน โดยพื้นฐานแล้วโค้ดของคุณจะไม่คอมไพล์ - ถ้าจะให้แม่นยำ
Royi Namir

คำตอบ:


101

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

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

ฉันขอแนะนำให้คุณลองใช้ลำดับการดำเนินการที่ใหญ่ขึ้นเล็กน้อยกับทั้งสองawaitและTask.ContinueWith- มันสามารถเปิดหูเปิดตาได้อย่างแท้จริง


2
การจัดการข้อผิดพลาดระหว่างตัวอย่างข้อมูลทั้งสองก็แตกต่างกันเช่นกัน โดยทั่วไปแล้วการทำงานในเรื่องนั้นจะง่ายawaitกว่า ContinueWith
Servy

@Servy: ทรูจะเพิ่มอะไรประมาณนั้น
Jon Skeet

1
การจัดตารางเวลาก็ค่อนข้างแตกต่างกันเช่นบริบทใดparseDataดำเนินการ
Stephen Cleary

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

4
@ แฮร์ริสัน: ลองนึกภาพคุณกำลังเขียนแอป WinForms - ถ้าคุณเขียนวิธีการ async โดยค่าเริ่มต้นโค้ดทั้งหมดในเมธอดจะทำงานในเธรด UI เนื่องจากความต่อเนื่องจะถูกกำหนดไว้ที่นั่น หากคุณไม่ระบุตำแหน่งที่คุณต้องการให้รันต่อเนื่องฉันไม่รู้ว่าค่าดีฟอลต์คืออะไร แต่มันอาจจบลงด้วยการรันบนเธรดพูลเธรดได้อย่างง่ายดาย ... ณ จุดนั้นคุณไม่สามารถเข้าถึง UI ได้ ฯลฯ .
Jon Skeet

100

นี่คือลำดับของข้อมูลโค้ดที่ฉันเพิ่งใช้เพื่อแสดงความแตกต่างและปัญหาต่างๆโดยใช้การแก้ปัญหาแบบ async

สมมติว่าคุณมีตัวจัดการเหตุการณ์บางอย่างในแอปพลิเคชันที่ใช้ GUI ซึ่งใช้เวลามากและคุณต้องการทำให้เป็นแบบอะซิงโครนัส นี่คือตรรกะแบบซิงโครนัสที่คุณเริ่มต้นด้วย:

while (true) {
    string result = LoadNextItem().Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
        break;
    }
}

LoadNextItem ส่งคืนงานซึ่งในที่สุดจะให้ผลลัพธ์บางอย่างที่คุณต้องการตรวจสอบ หากผลลัพธ์ปัจจุบันเป็นผลลัพธ์ที่คุณต้องการคุณจะอัปเดตค่าของตัวนับบางตัวบน UI และส่งกลับจากวิธีการ มิฉะนั้นคุณยังคงประมวลผลรายการเพิ่มเติมจาก LoadNextItem

แนวคิดแรกสำหรับเวอร์ชันอะซิงโครนัส: ใช้การต่อเนื่อง! และเราจะไม่สนใจส่วนที่วนซ้ำในขณะนี้ ฉันหมายความว่ามีอะไรผิดพลาดบ้าง?

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
});

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

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

เยี่ยมมากตอนนี้เรามีวิธีที่ไม่ผิดพลาด! มันล้มเหลวอย่างเงียบ ๆ แทน ความต่อเนื่องเป็นงานที่แยกจากกันโดยที่สถานะของพวกเขาไม่ได้เชื่อมโยงกับภารกิจก่อนหน้า ดังนั้นแม้ว่า LoadNextItem จะเกิดข้อผิดพลาดผู้เรียกจะเห็นเฉพาะงานที่ทำสำเร็จแล้ว โอเคจากนั้นส่งต่อข้อยกเว้นหากมี:

return LoadNextItem().ContinueWith(t => {
    if (t.Exception != null) {
        throw t.Exception.InnerException;
    }
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

เยี่ยมมากตอนนี้ใช้งานได้จริง สำหรับรายการเดียว ทีนี้การวนลูปนั้นเป็นอย่างไร ปรากฎว่าโซลูชันที่เทียบเท่ากับตรรกะของเวอร์ชันซิงโครนัสดั้งเดิมจะมีลักษณะดังนี้:

Task AsyncLoop() {
    return AsyncLoopTask().ContinueWith(t =>
        Counter.Value = t.Result,
        TaskScheduler.FromCurrentSynchronizationContext());
}
Task<int> AsyncLoopTask() {
    var tcs = new TaskCompletionSource<int>();
    DoIteration(tcs);
    return tcs.Task;
}
void DoIteration(TaskCompletionSource<int> tcs) {
    LoadNextItem().ContinueWith(t => {
        if (t.Exception != null) {
            tcs.TrySetException(t.Exception.InnerException);
        } else if (t.Result.Contains("target")) {
            tcs.TrySetResult(t.Result.Length);
        } else {
            DoIteration(tcs);
        }});
}

หรือแทนที่จะใช้ทั้งหมดข้างต้นคุณสามารถใช้ async เพื่อทำสิ่งเดียวกัน:

async Task AsyncLoop() {
    while (true) {
        string result = await LoadNextItem();
        if (result.Contains("target")) {
            Counter.Value = result.Length;
            break;
        }
    }
}

ตอนนี้ดีกว่ามากแล้วใช่ไหม


ขอบคุณคำอธิบายที่ดีจริงๆ
Elger Mensonides

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