ทำไมต้องทำงานต่อเนื่องเมื่องานทั้งหมดถูกดำเนินการพร้อมกัน?


14

ฉันเพิ่งสังเกตข้อสงสัยเกี่ยวกับTask.WhenAllวิธีการเมื่อทำงานบน. NET Core 3.0 ฉันส่งTask.Delayงานง่าย ๆเป็นอาร์กิวเมนต์เดียวไปยังTask.WhenAllและฉันคาดว่างานที่ถูกห่อจะทำงานเหมือนกันกับงานต้นฉบับ แต่นี่ไม่ใช่กรณี ความต่อเนื่องของงานดั้งเดิมจะถูกดำเนินการแบบอะซิงโครนัส (ซึ่งเป็นที่ต้องการ) และความต่อเนื่องของหลาย ๆ ตัวTask.WhenAll(task)จะถูกดำเนินการแบบซิงโครนัสแบบหนึ่งหลังจากสิ่งอื่น ๆ

นี่คือตัวอย่างของพฤติกรรมนี้ งานของผู้ปฏิบัติงานสี่คนกำลังรอTask.Delayงานเดียวกันให้เสร็จและดำเนินการคำนวณต่อไปอย่างหนัก (จำลองโดย a Thread.Sleep)

var task = Task.Delay(500);
var workers = Enumerable.Range(1, 4).Select(async x =>
{
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
        $" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} before await");

    await task;
    //await Task.WhenAll(task);

    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
        $" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} after await");

    Thread.Sleep(1000); // Simulate some heavy CPU-bound computation
}).ToArray();
Task.WaitAll(workers);

นี่คือผลลัพธ์ การต่อเนื่องทั้งสี่แบบกำลังทำงานตามที่คาดไว้ในเธรดที่ต่างกัน (แบบขนาน)

05:23:25.511 [1] Worker1 before await
05:23:25.542 [1] Worker2 before await
05:23:25.543 [1] Worker3 before await
05:23:25.543 [1] Worker4 before await
05:23:25.610 [4] Worker1 after await
05:23:25.610 [7] Worker2 after await
05:23:25.610 [6] Worker3 after await
05:23:25.610 [5] Worker4 after await

ตอนนี้ถ้าฉันแสดงความคิดเห็นบรรทัดawait taskและ uncomment บรรทัดต่อไปนี้await Task.WhenAll(task)เอาท์พุทค่อนข้างแตกต่าง การดำเนินการต่อทั้งหมดกำลังทำงานในเธรดเดียวกันดังนั้นการคำนวณจึงไม่ขนานกัน การคำนวณแต่ละครั้งจะเริ่มขึ้นหลังจากเสร็จสิ้นการคำนวณก่อนหน้า:

05:23:46.550 [1] Worker1 before await
05:23:46.575 [1] Worker2 before await
05:23:46.576 [1] Worker3 before await
05:23:46.576 [1] Worker4 before await
05:23:46.645 [4] Worker1 after await
05:23:47.648 [4] Worker2 after await
05:23:48.650 [4] Worker3 after await
05:23:49.651 [4] Worker4 after await

น่าประหลาดใจที่สิ่งนี้เกิดขึ้นเฉพาะเมื่อพนักงานแต่ละคนรอเสื้อคลุมที่แตกต่างกัน ถ้าฉันกำหนด wrapper upline:

var task = Task.WhenAll(Task.Delay(500));

... และจากนั้นawaitภารกิจเดียวกันภายในพนักงานทุกคนพฤติกรรมจะเหมือนกันกับกรณีแรก

คำถามของฉันคือ:ทำไมสิ่งนี้เกิดขึ้น? อะไรทำให้ความต่อเนื่องของ wrappers ต่าง ๆ ของภารกิจเดียวกันรันในเธรดเดียวกันพร้อมกัน?

หมายเหตุ: การห่อภารกิจด้วยTask.WhenAnyแทนที่จะเป็นTask.WhenAllผลลัพธ์ในลักษณะการทำงานที่แปลกประหลาดเหมือนกัน

ข้อสังเกตอีกอย่าง:ฉันคาดว่าการห่อตัวเสื้อคลุมไว้ภายใน a Task.Runจะทำให้การประสานต่อเนื่องไม่ตรงกัน แต่มันไม่ได้เกิดขึ้น ความต่อเนื่องของบรรทัดด้านล่างยังคงดำเนินการอยู่ในเธรดเดียวกัน (พร้อมกัน)

await Task.Run(async () => await Task.WhenAll(task));

ชี้แจง:ความแตกต่างดังกล่าวข้างต้นพบในแอปพลิเคชันคอนโซลที่ทำงานบนแพลตฟอร์ม. NET Core 3.0 บน. NET Framework 4.8 ไม่มีความแตกต่างระหว่างการรองานต้นฉบับหรือ wrapper งาน ในทั้งสองกรณีดำเนินการต่อเนื่องพร้อมกันในเธรดเดียวกัน


แค่อยากรู้อยากเห็นจะเกิดอะไรขึ้นถ้าawait Task.WhenAll(new[] { task });?
vasily.sib

1
ฉันคิดว่าเป็นเพราะการลัดวงจรภายในTask.WhenAll
Michael Randall

3
LinqPad ให้ผลลัพธ์ที่สองที่เหมือนกันสำหรับทั้งสองรุ่น ... สภาพแวดล้อมที่คุณใช้ในการเรียกใช้การทำงานแบบขนาน (คอนโซลกับ WinForms vs ... ,.
Alexei Levenkov

1
ฉันสามารถที่จะทำซ้ำพฤติกรรมนี้ใน. NET Core 3.0 และ 3.1 แต่หลังจากเปลี่ยนเริ่มต้นTask.Delayจาก100เป็น1000เพื่อที่จะไม่สมบูรณ์เมื่อawaited
Stephen Cleary

2
@ BlueStrat ดีค้นหา! มันอาจจะเกี่ยวข้องอย่างใด ที่น่าสนใจฉันไม่สามารถทำซ้ำพฤติกรรมที่ผิดพลาดของรหัสของ Microsoft ใน. NET Frameworks 4.6, 4.6.1, 4.7.1, 4.7.2 และ 4.8 ฉันได้รับรหัสด้ายที่แตกต่างกันทุกครั้งซึ่งเป็นพฤติกรรมที่ถูกต้อง นี่คือซอที่เล่นบน 4.7.2
Theodor Zoulias

คำตอบ:


2

ดังนั้นคุณจึงมีวิธีการซิงค์หลายวิธีที่รอตัวแปรงานเดียวกัน

    await task;
    // CPU heavy operation

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

หากคุณต้องการให้แต่ละความต่อเนื่องในการทำงานแบบอะซิงโครนัสคุณอาจต้องการบางสิ่งเช่นนี้

    await task;
    await Task.Yield().ConfigureAwait(false);
    // CPU heavy operation

เพื่อให้งานของคุณกลับมาจากความต่อเนื่องเริ่มต้นและช่วยให้ภาระของ CPU SynchronizationContextในการทำงานด้านนอกของ


ขอบคุณ Jeremy สำหรับคำตอบ ใช่Task.Yieldเป็นทางออกที่ดีสำหรับปัญหาของฉัน แม้ว่าคำถามของฉันจะเพิ่มเติมเกี่ยวกับสาเหตุที่เกิดขึ้นนี้และน้อยกว่าเกี่ยวกับวิธีการบังคับพฤติกรรมที่ต้องการ
Theodor Zoulias

หากคุณต้องการทราบจริงๆซอร์สโค้ดอยู่ที่นี่; github.com/microsoft/referencesource/blob/master/mscorlib/…
Jeremy Lakeman

ฉันหวังว่ามันง่ายมากที่จะได้คำตอบสำหรับคำถามของฉันโดยการศึกษาซอร์สโค้ดของคลาสที่เกี่ยวข้อง มันจะพาฉันทุกวัยเพื่อทำความเข้าใจกับรหัสและหาว่าเกิดอะไรขึ้น!
Theodor Zoulias

กุญแจสำคัญคือการหลีกเลี่ยงการSynchronizationContextเรียกConfigureAwait(false)ครั้งเดียวกับงานดั้งเดิมอาจจะเพียงพอ
Jeremy Lakeman

นี่คือแอปพลิเคชันคอนโซลและSynchronizationContext.Currentเป็นโมฆะ แต่ฉันเพิ่งตรวจสอบเพื่อให้แน่ใจ ฉันเพิ่มConfigureAwait(false)ในawaitบรรทัดและมันก็ไม่ได้ทำให้แตกต่าง ข้อสังเกตเหมือนกันก่อนหน้านี้
Theodor Zoulias

1

เมื่อเป็นงานที่สร้างขึ้นโดยใช้Task.Delay()ตัวเลือกการสร้างถูกตั้งค่าให้มากกว่าNoneRunContinuationsAsychronously

นี่อาจเป็นการทำลายการเปลี่ยนแปลงระหว่าง. net framework และ. net core ดูเหมือนว่าจะอธิบายพฤติกรรมที่คุณกำลังสังเกตอยู่ นอกจากนี้คุณยังสามารถตรวจสอบได้จากการขุดเป็นรหัสที่มาที่Task.Delay()ถูกnewing ขึ้นDelayPromiseซึ่งเรียกค่าเริ่มต้นTaskคอนสตรัคออกจากตัวเลือกการสร้างไม่ระบุ


ขอบคุณ Tanveer สำหรับคำตอบ ดังนั้นคุณคาดการณ์ว่าใน. NET Core RunContinuationsAsychronouslyกลายเป็นค่าเริ่มต้นแทนNoneเมื่อสร้างTaskวัตถุใหม่ นี่จะอธิบายการสังเกตของฉันบางส่วน แต่ไม่ใช่ทั้งหมด โดยเฉพาะมันจะไม่อธิบายความแตกต่างระหว่างการรอคอยTask.WhenAllwrapper เดียวกันและรอคอย wrapper ที่แตกต่างกัน
Theodor Zoulias

0

ในรหัสของคุณรหัสต่อไปนี้อยู่นอกเนื้อหาที่เกิดซ้ำ

var task = Task.Delay(100);

ดังนั้นทุกครั้งที่คุณเรียกใช้สิ่งต่อไปนี้มันจะรองานและรันในเธรดแยกต่างหาก

await task;

แต่ถ้าคุณรันสิ่งต่อไปนี้มันจะทำการตรวจสอบสถานะของtaskมันดังนั้นมันจะรันในเธรดเดียว

await Task.WhenAll(task);

แต่ถ้าคุณย้ายการสร้างงานไปWhenAllมันจะทำงานแต่ละงานในเธรดแยก

var task = Task.Delay(100);
await Task.WhenAll(task);

ขอบคุณ Seyedraouf สำหรับคำตอบ คำอธิบายของคุณไม่ได้ฟังดูน่าพอใจสำหรับฉัน งานที่ส่งกลับโดยTask.WhenAllเป็นเพียงปกติเช่นเดิมTask taskงานทั้งสองเสร็จสมบูรณ์ในบางจุดต้นฉบับเป็นผลมาจากเหตุการณ์จับเวลาและคอมโพสิตอันเป็นผลมาจากความสำเร็จของงานต้นฉบับ ทำไมความต่อเนื่องของพวกเขาจึงแสดงพฤติกรรมที่แตกต่างกัน? ภารกิจใดที่แตกต่างจากงานอื่น?
Theodor Zoulias
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.