จะเกิดอะไรขึ้นเมื่อเธรดรองานในขณะที่ลูป?


10

หลังจากจัดการกับรูปแบบ async / คอยรูปของ C # ในขณะนี้ฉันก็ตระหนักว่าฉันไม่รู้วิธีอธิบายสิ่งที่เกิดขึ้นในรหัสต่อไปนี้:

async void MyThread()
{
    while (!_quit)
    {
        await GetWorkAsync();
    }
}

GetWorkAsync()จะถือว่าคืนค่าTaskที่ไม่สามารถรอได้ซึ่งอาจหรือไม่อาจทำให้เกิดการสลับเธรดเมื่อดำเนินการต่อเนื่อง

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

อย่างไรก็ตามในวงแนวคิดของ "ส่วนที่เหลือของวิธีการ" ทำให้ฉันมีหมอกเล็กน้อย

จะเกิดอะไรขึ้นกับ "ส่วนที่เหลือของลูป" หากเธรดเปิดอยู่ต่อเนื่องกับหากไม่เปลี่ยน เธรดใดที่วนซ้ำครั้งถัดไปของลูปที่เรียกใช้งาน

การสังเกตของฉันแสดง (ไม่ได้รับการตรวจสอบอย่างแน่ชัด) ว่าการวนซ้ำแต่ละครั้งเริ่มต้นในเธรดเดียวกัน (ฉบับดั้งเดิม) ในขณะที่การดำเนินการต่อเนื่องจะดำเนินการอีกครั้ง สิ่งนี้เป็นจริงได้ไหม ถ้าใช่นี่คือระดับของการขนานที่ไม่คาดคิดซึ่งจำเป็นต้องคำนึงถึงความปลอดภัยของเธรดแบบ vis-a-vis ของเมธอด GetWorkAsync หรือไม่?

UPDATE: คำถามของฉันไม่เหมือนกันตามที่แนะนำโดยบางคน while (!_quit) { ... }รูปแบบรหัสเป็นเพียงความเรียบง่ายของรหัสที่เกิดขึ้นจริงของฉัน ในความเป็นจริงเธรดของฉันคือวนรอบที่ยาวนานซึ่งประมวลผลคิวอินพุตของไอเท็มงานในช่วงเวลาปกติ (ทุกๆ 5 วินาทีโดยค่าเริ่มต้น) การตรวจสอบเงื่อนไขการเลิกใช้จริงไม่ใช่การตรวจสอบสนามอย่างง่ายตามที่แนะนำโดยรหัสตัวอย่าง แต่เป็นการตรวจสอบการจัดการเหตุการณ์



1
ดูเพิ่มเติมได้อย่างไรผลผลิตและการรอคอยใช้การควบคุมการไหลใน. NET? สำหรับข้อมูลที่ดีเกี่ยวกับการเชื่อมต่อทั้งหมดเข้าด้วยกัน
จอห์นวู

@ จอห์นวู: ฉันยังไม่เห็นด้ายดังนั้น มีนักเก็ตข้อมูลที่น่าสนใจมากมาย ขอบคุณ!
aoven

คำตอบ:


6

จริงๆคุณสามารถตรวจสอบออกที่ลองโรสลิน วิธีการที่รอคอยของคุณจะถูกเขียนใหม่ในvoid IAsyncStateMachine.MoveNext()คลาส async ที่สร้างขึ้น

สิ่งที่คุณจะเห็นคืออะไรเช่นนี้:

            if (this.state != 0)
                goto label_2;
            //set up the state machine here
            label_1:
            taskAwaiter.GetResult();
            taskAwaiter = default(TaskAwaiter);
            label_2:
            if (!OuterClass._quit)
            {
               taskAwaiter = GetWorkAsync().GetAwaiter();
               //state machine stuff here
            }
            goto label_1;

โดยพื้นฐานแล้วไม่สำคัญว่าคุณจะใช้เธรดใด เครื่องสถานะสามารถทำงานต่อได้อย่างถูกต้องโดยแทนที่ลูปของคุณด้วยโครงสร้าง if / goto ที่เทียบเท่า

ต้องบอกว่าวิธี async ไม่จำเป็นต้องดำเนินการในหัวข้อที่แตกต่างกัน ดูคำอธิบายของ Eric Lippert "มันไม่ใช่เวทมนต์"เพื่ออธิบายวิธีที่คุณสามารถทำงานasync/awaitกับเธรดเดียวเท่านั้น


2
ฉันดูเหมือนจะประเมินขอบเขตของการเขียนซ้ำที่คอมไพเลอร์ทำกับรหัส async ของฉัน ในสาระสำคัญไม่มี "วน" หลังจากเขียนใหม่! นั่นคือส่วนที่ขาดหายไปสำหรับฉัน ยอดเยี่ยมและขอบคุณสำหรับลิงค์ 'ลองโรสลิน' เช่นกัน!
aoven

GOTO เป็นโครงสร้างลูปดั้งเดิม อย่าลืมว่า

2

ประการแรก Servy ได้เขียนโค้ดบางส่วนในคำตอบของคำถามที่คล้ายกันซึ่งขึ้นอยู่กับคำตอบนี้:

/programming/22049339/how-to-create-a-cancellable-task-loop

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

    private static Task GetWorkWhileNotQuit()
    {
        var tcs = new TaskCompletionSource<bool>();

        Task previous = Task.FromResult(_quit);
        Action<Task> continuation = null;
        continuation = t =>
        {
            if (!_quit)
            {
                previous = previous.ContinueWith(_ => GetWorkAsync())
                    .Unwrap()
                    .ContinueWith(_ => previous.ContinueWith(continuation));
            }
            else
            {
                tcs.SetResult(_quit);
            }
        };
        previous.ContinueWith(continuation);
        return tcs.Task;
    }

ขั้นตอนนี้ใช้เวลาในการพันศีรษะ แต่โดยสรุป:

  • continuationหมายถึงการปิดสำหรับ"การวนซ้ำปัจจุบัน"
  • previousแสดงถึงTaskสถานะของ"การทำซ้ำก่อนหน้า" ที่มีอยู่ (นั่นคือมันรู้เมื่อ 'การทำซ้ำ' เสร็จสิ้นและใช้สำหรับการเริ่มต้นต่อไป .. )
  • สมมติว่าGetWorkAsync()คืนค่า a Taskซึ่งหมายความว่าContinueWith(_ => GetWorkAsync())จะส่งคืนTask<Task>การเรียกUnwrap()เพื่อรับ 'ภารกิจภายใน' (เช่นผลลัพธ์จริงของGetWorkAsync())

ดังนั้น:

  1. ตอนแรกไม่มีก่อนหน้านี้ซ้ำดังนั้นจึงได้รับมอบหมายเพียงค่าของTask.FromResult(_quit) - Task.Completed == trueรัฐเริ่มออกเป็น
  2. continuationมีการเรียกใช้เป็นครั้งแรกที่ใช้previous.ContinueWith(continuation)
  3. การcontinuationอัปเดตการปิดpreviousเพื่อสะท้อนให้เห็นถึงสถานะความสมบูรณ์ของ_ => GetWorkAsync()
  4. เมื่อ_ => GetWorkAsync()เสร็จแล้วมัน "ดำเนินการต่อ" _previous.ContinueWith(continuation)- คือเรียกcontinuationแลมบ์ดาอีกครั้ง
    • เห็นได้ชัดว่าที่จุดนี้previousได้รับการปรับปรุงด้วยสถานะของ_ => GetWorkAsync()ดังนั้นcontinuationแลมบ์ดาจะถูกเรียกเมื่อGetWorkAsync()ผลตอบแทน

continuationแลมบ์ดาจะตรวจสอบสถานะของ_quitดังนั้นหาก_quit == falseแล้วไม่มีตมากขึ้นและTaskCompletionSourceได้รับการกำหนดค่าของ_quitและทุกอย่างจะเสร็จสมบูรณ์

ในฐานะที่เป็นสำหรับการสังเกตของคุณเกี่ยวกับความต่อเนื่องที่กำลังดำเนินการในหัวข้อที่แตกต่างกันที่ไม่ได้เป็นสิ่งที่async/ awaitคำหลักที่จะทำเพื่อคุณตามบล็อกนี้"งานคือ (ยัง) ไม่ได้กระทู้และ async ไม่ขนาน" - https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/

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


2
รหัสที่ฉันแสดงคือเรียบง่ายมาก ภายใน GetWorkAsync () มีหลายรออีก บางคนเข้าถึงฐานข้อมูลและเครือข่ายซึ่งหมายถึง I / O จริง ดังที่ฉันเข้าใจแล้วการสลับเธรดนั้นเป็นผลลัพธ์ตามธรรมชาติ (ไม่จำเป็นต้องมี) เนื่องจากการรอเช่นนั้นเนื่องจากเธรดเริ่มต้นไม่ได้สร้างบริบทการซิงโครไนซ์ใด ๆ ที่จะควบคุมตำแหน่งที่ควรดำเนินการต่อไป ดังนั้นพวกเขาจึงรันบนเธรดพูลเธรด เหตุผลของฉันผิดหรือเปล่า?
aoven

@ aoven Good point - ฉันไม่ได้พิจารณาถึงความแตกต่างของSynchronizationContext- นั่นเป็นสิ่งสำคัญอย่างยิ่งเนื่องจาก.ContinueWith()ใช้ SynchronizationContext เพื่อส่งต่อความต่อเนื่อง มันจะอธิบายพฤติกรรมที่คุณเห็นอย่างแน่นอนหากawaitมีการเรียกใช้เธรด ThreadPool หรือเธรด ASP.NET ความต่อเนื่องอาจถูกส่งไปยังเธรดอื่นในกรณีเหล่านั้น ในทางกลับกันการเรียกawaitใช้บริบทแบบเธรดเดียวเช่นบริบท WPF Dispatcher หรือ Winforms ควรจะเพียงพอเพื่อให้แน่ใจว่าการดำเนินการต่อจะเกิดขึ้นกับต้นฉบับ กระทู้
Ben Cottrell
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.