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


242

ครั้งแล้วครั้งเล่าฉันเห็นว่าการใช้async- awaitไม่ได้สร้างกระทู้เพิ่มเติม ไม่เป็นไรเพราะวิธีการเดียวที่คอมพิวเตอร์สามารถทำได้มากกว่า 1 อย่างในเวลาเดียวคือ

  • จริง ๆ แล้วทำสิ่งต่าง ๆ มากกว่า 1 ครั้ง (ดำเนินการแบบขนานใช้หลายโปรเซสเซอร์)
  • จำลองสถานการณ์โดยการกำหนดตารางงานและสลับไปมาระหว่างกัน (ทำ A เล็กน้อย, B เล็กน้อย, A เล็กน้อย, ฯลฯ )

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


17
งาน IO ไม่ได้เชื่อมโยงกับ CPU ดังนั้นจึงไม่จำเป็นต้องมีเธรด จุดหลักของ async คือการไม่บล็อกเธรดระหว่างภารกิจที่ถูกผูกไว้ของ IO
juharr

24
@jdweng: ไม่ไม่เลย แม้ว่ามันจะสร้างเธรดใหม่แต่ก็แตกต่างจากการสร้างกระบวนการใหม่
Jon Skeet

8
หากคุณเข้าใจการเขียนโปรแกรมไม่ตรงกันโทรกลับตามแล้วคุณเข้าใจวิธีการawait/ asyncงานโดยไม่ต้องสร้างหัวข้อใด ๆ
user253751

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

6
@RubberDuck: ใช่มันอาจใช้เธรดจากเธรดพูลเพื่อความต่อเนื่อง แต่มันไม่ได้เริ่มต้นเธรดในวิธีที่ OP จินตนาการที่นี่ - มันไม่เหมือนที่มันบอกว่า "ใช้วิธีการธรรมดานี้ตอนนี้รันในเธรดแยกต่างหาก - นั่นคือ async" มันฉลาดกว่านั้นมาก
Jon Skeet

คำตอบ:


299

ที่จริงแล้ว async / คอยไม่ได้เป็นเวทมนตร์ หัวข้อเต็มค่อนข้างกว้าง แต่สำหรับคำตอบสำหรับคำถามของคุณฉันคิดว่าเราสามารถจัดการได้

มาจัดการกับเหตุการณ์การคลิกปุ่มง่ายๆในแอปพลิเคชัน Windows Forms:

public async void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before awaiting");
    await GetSomethingAsync();
    Console.WriteLine("after awaiting");
}

ฉันจะชัดเจน ไม่ได้พูดคุยเกี่ยวกับสิ่งที่เป็นGetSomethingAsyncอยู่ในตอนนี้กลับมา สมมุติว่านี่คือสิ่งที่จะทำให้เสร็จหลังจากนั้น 2 วินาที

ในโลกแบบดั้งเดิมที่ไม่ประสานเวลาตัวจัดการเหตุการณ์การคลิกปุ่มของคุณจะมีลักษณะดังนี้:

public void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before waiting");
    DoSomethingThatTakes2Seconds();
    Console.WriteLine("after waiting");
}

เมื่อคุณคลิกปุ่มในแบบฟอร์มแอปพลิเคชันจะปรากฏขึ้นค้างประมาณ 2 วินาทีในขณะที่เรารอให้วิธีนี้เสร็จสมบูรณ์ สิ่งที่เกิดขึ้นคือ "message pump" ซึ่งโดยทั่วไปจะเป็น loop จะถูกบล็อค

ลูปนี้ถาม windows อย่างต่อเนื่อง "มีใครทำอะไรบางอย่างเช่นเลื่อนเมาส์คลิกที่บางสิ่งบางอย่างหรือไม่ฉันต้องทาสีบางสิ่งบางอย่างหรือไม่ถ้าใช่บอกฉัน!" แล้วประมวลผลว่า "บางสิ่ง" การวนซ้ำนี้มีข้อความว่าผู้ใช้คลิกที่ "button1" (หรือประเภทข้อความเทียบเท่าจาก Windows) และสิ้นสุดการเรียกbutton1_Clickวิธีการของเราด้านบน จนกว่าเมธอดนี้จะส่งคืนลูปนี้จะหยุดรอ ขั้นตอนนี้ใช้เวลา 2 วินาทีและในระหว่างนี้จะไม่มีการประมวลผลข้อความ

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

ดังนั้นถ้าในตัวอย่างแรกasync/awaitไม่ได้สร้างเธรดใหม่มันจะทำอย่างไร

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

  1. รหัสทั้งหมดที่นำไปสู่awaitรวมถึงการโทรไปยังGetSomethingAsync
  2. รหัสทั้งหมดดังต่อไปนี้ await

ภาพประกอบ:

code... code... code... await X(); ... code... code... code...

จัดใหม่:

code... code... code... var x = X(); await X; code... code... code...
^                                  ^          ^                     ^
+---- portion 1 -------------------+          +---- portion 2 ------+

โดยทั่วไปวิธีการดำเนินการเช่นนี้:

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

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

  3. สิ่งที่awaitคำหลักพร้อมกับมายากลคอมไพเลอร์ที่ฉลาดทำก็คือมันเป็นอะไรบางอย่างเช่น "โอเคคุณรู้อะไรฉันจะกลับมาจากปุ่มคลิกตัวจัดการเหตุการณ์ที่นี่เมื่อคุณ (ในสิ่งที่เรา ' กำลังรออีกรอบ) ทำให้ฉันรู้เพราะฉันยังมีรหัสเหลือให้เปิดใช้งาน "

    ที่จริงแล้วมันจะให้คลาส SynchronizationContextรู้ว่ามันทำเสร็จแล้วซึ่งขึ้นอยู่กับบริบทการซิงโครไนซ์ที่เกิดขึ้นจริงที่กำลังเล่นอยู่ในขณะนี้จะเข้าคิวเพื่อดำเนินการ คลาสบริบทที่ใช้ในโปรแกรม Windows Forms จะจัดคิวโดยใช้คิวที่วนรอบข้อความถูกปั๊ม

  4. ดังนั้นจึงกลับไปที่ลูปข้อความซึ่งตอนนี้สามารถปั๊มข้อความต่อไปได้เช่นย้ายหน้าต่างปรับขนาดหรือคลิกปุ่มอื่น

    สำหรับผู้ใช้ UI จะตอบสนองอีกครั้งโดยประมวลผลการคลิกปุ่มอื่น ๆ การปรับขนาดและที่สำคัญที่สุดคือการวาดใหม่ดังนั้นจึงไม่ปรากฏว่าหยุดการทำงาน

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

มีชิ้นส่วนที่เคลื่อนไหวอยู่ใต้กระโปรงหน้ารถนี่คือบางส่วนที่เชื่อมโยงไปยังข้อมูลเพิ่มเติมฉันจะบอกว่า "คุณต้องการมัน" แต่หัวข้อนี้ค่อนข้างกว้างและมันค่อนข้างสำคัญที่จะต้องรู้ว่าชิ้นส่วนที่เคลื่อนไหวเหล่านั้นเป็นอย่างไร อย่างสม่ำเสมอคุณจะเข้าใจว่า async / await ยังคงเป็นแนวคิดรั่วไหล ข้อ จำกัด และปัญหาพื้นฐานบางประการยังคงรั่วไหลไปยังโค้ดที่อยู่รอบ ๆ และหากไม่เป็นเช่นนั้นคุณมักจะต้องตรวจแก้จุดบกพร่องของแอปพลิเคชันที่แตกตัวแบบสุ่มโดยไม่มีเหตุผลที่ดี


ตกลงดังนั้นจะเกิดอะไรGetSomethingAsyncขึ้นถ้าหมุนเธรดที่จะเสร็จสมบูรณ์ภายใน 2 วินาที ใช่แล้วเห็นได้ชัดว่ามีหัวข้อใหม่ในการเล่น อย่างไรก็ตามเธรดนี้ไม่ได้เป็นเพราะ async-ness ของวิธีนี้เป็นเพราะโปรแกรมเมอร์ของวิธีนี้เลือกเธรดที่จะใช้โค้ดอะซิงโครนัส I / O อะซิงโครนัสเกือบทั้งหมดไม่ใช้เธรดพวกเขาใช้สิ่งต่าง ๆ async/await ด้วยตัวเองไม่หมุนกระทู้ใหม่ แต่แน่นอนว่า "สิ่งที่เรารอ" อาจถูกนำไปใช้โดยใช้เธรด

มีหลายสิ่งใน. NET ที่ไม่จำเป็นต้องหมุนเธรดด้วยตนเอง แต่ยังคงไม่พร้อมกัน:

  • คำขอทางเว็บ (และสิ่งอื่น ๆ ที่เกี่ยวข้องกับเครือข่ายที่ต้องใช้เวลา)
  • การอ่านและการเขียนไฟล์แบบอะซิงโครนัส
  • และอีกมากมายสัญญาณที่ดีคือถ้าคลาส / อินเทอร์เฟซที่สงสัยมีวิธีการตั้งชื่อSomethingSomethingAsyncหรือBeginSomethingและEndSomethingและมีIAsyncResultส่วนเกี่ยวข้อง

สิ่งเหล่านี้มักจะไม่ใช้ด้ายใต้กระโปรงหน้ารถ


ตกลงดังนั้นคุณต้องการบางสิ่งที่ "หัวข้อหัวข้อกว้าง"?

ทีนี้ลองถามโรสลินเกี่ยวกับการคลิกปุ่มของเรา:

ลองโรสลิน

ฉันจะไม่เชื่อมโยงในชั้นเรียนที่สร้างขึ้นเต็มรูปแบบที่นี่ แต่เป็นสิ่งที่เต็มไปด้วยเลือด


11
ดังนั้นโดยทั่วไปแล้วสิ่งที่ OP อธิบายว่า " การจำลองการประมวลผลแบบขนานโดยการกำหนดเวลางานและสลับระหว่างพวกเขา " ใช่ไหม
Bergi

4
@ Bergi ไม่ค่อยมาก การดำเนินการแบบขนานอย่างแท้จริง - งาน I / O แบบอะซิงโครนัสยังดำเนินอยู่และไม่ต้องใช้เธรดเพื่อดำเนินการต่อ (นี่เป็นสิ่งที่มีการใช้งานมานานก่อนที่ Windows จะมาถึง - MS DOS ใช้ I / O แบบอะซิงโครนัสด้วยเช่นกัน มีหลายเธรด!) แน่นอนคุณawait สามารถใช้วิธีที่คุณอธิบายได้เช่นกัน แต่โดยทั่วไปแล้วไม่ใช่ เฉพาะการโทรกลับที่กำหนดเวลาไว้ (บนพูลเธรด) - ระหว่างการติดต่อกลับและการร้องขอไม่จำเป็นต้องมีเธรด
Luaan

3
นั่นเป็นสาเหตุที่ฉันต้องการหลีกเลี่ยงการพูดมากเกินไปเกี่ยวกับวิธีการที่ทำอย่างชัดเจนเนื่องจากคำถามเกี่ยวกับ async / waitit โดยเฉพาะซึ่งไม่ได้สร้างเธรดของตัวเอง เห็นได้ชัดว่าพวกเขาสามารถนำมาใช้เพื่อรอสำหรับหัวข้อที่จะเสร็จสมบูรณ์
Lasse V. Karlsen

6
@ LasseV.Karlsen - ฉันกินคำตอบที่ดีของคุณ แต่ฉันยังคงแขวนอยู่ในรายละเอียด ผมเข้าใจว่าจัดการเหตุการณ์ที่มีอยู่ในขณะที่ขั้นตอนที่ 4 ซึ่งจะช่วยให้ปั๊มข้อความที่จะดำเนินการสูบน้ำ แต่เมื่อและที่ไม่ว่า "สิ่งที่จะใช้เวลาสองวินาที" ยังคงดำเนินถ้าไม่ได้อยู่ในหัวข้อแยกต่างหาก? หากมีการดำเนินการในหัวข้อ UI แล้วมันจะปิดกั้นปั๊มข้อความต่อไปขณะที่การดำเนินการเพราะมีการดำเนินการบางครั้งในหัวข้อเดียวกัน .. [ยังคง] ...
rory.ap

3
ฉันชอบคำอธิบายของคุณด้วยปั๊มข้อความ คำอธิบายของคุณแตกต่างกันอย่างไรเมื่อไม่มีปั๊มข้อความเหมือนกับในคอนโซลแอปพลิเคชันหรือเว็บเซิร์ฟเวอร์ วิธีการ reentrace ของวิธีการประสบความสำเร็จ?
Puchacz

95

ผมอธิบายในเต็มรูปแบบในการโพสต์บล็อกของฉันไม่มีกระทู้คือ

กล่าวโดยสรุประบบ I / O ที่ทันสมัยทำให้การใช้งาน DMA (การเข้าถึงหน่วยความจำโดยตรง) อย่างหนัก มีโปรเซสเซอร์พิเศษเฉพาะสำหรับการ์ดเครือข่าย, การ์ดวิดีโอ, ตัวควบคุม HDD, พอร์ตอนุกรม / ขนาน ฯลฯ โปรเซสเซอร์เหล่านี้มีการเข้าถึงโดยตรงไปยังหน่วยความจำบัสและจัดการการอ่าน / เขียนอย่างสมบูรณ์เป็นอิสระจาก CPU ซีพียูเพียงแค่ต้องแจ้งให้อุปกรณ์ทราบถึงตำแหน่งในหน่วยความจำที่มีข้อมูลและจากนั้นสามารถทำสิ่งต่าง ๆ ของตัวเองได้จนกว่าอุปกรณ์จะเพิ่มสัญญาณขัดจังหวะเพื่อแจ้งให้ CPU ทราบว่าการอ่าน / เขียนเสร็จสมบูรณ์

เมื่อการดำเนินการอยู่ในระหว่างการบินไม่มีงานใดที่ CPU จะทำและไม่มีเธรด


เพียงเพื่อให้ชัดเจน .. ฉันเข้าใจในระดับสูงว่าเกิดอะไรขึ้นเมื่อใช้ async-await เกี่ยวกับการไม่สร้างเธรด - ไม่มีเธรดเฉพาะในคำขอ I / O ไปยังอุปกรณ์ที่เหมือนคุณบอกว่ามีโปรเซสเซอร์ของตัวเองที่จัดการการร้องขอเอง? เราสามารถสมมติว่าคำขอ I / O ทั้งหมดได้รับการจัดการกับตัวประมวลผลอิสระซึ่งหมายถึงการใช้งาน Task.Run เฉพาะกับการกระทำที่ถูกผูกไว้ของ CPU?
Yonatan Nir

@YonatanNir: มันไม่ได้เป็นเพียงเกี่ยวกับโปรเซสเซอร์ที่แยกต่างหาก การตอบสนองที่ขับเคลื่อนด้วยเหตุการณ์ใด ๆ นั้นเป็นแบบอะซิงโครนัส Task.Runเหมาะสมที่สุดสำหรับการดำเนินการที่ผูกกับ CPUแต่มีการใช้งานเพียงเล็กน้อยเช่นกัน
Stephen Cleary

1
ฉันอ่านบทความของคุณเสร็จแล้วและยังมีสิ่งพื้นฐานที่ฉันไม่เข้าใจเนื่องจากฉันไม่คุ้นเคยกับการใช้ระบบปฏิบัติการระดับล่าง ฉันได้สิ่งที่คุณเขียนถึงที่คุณเขียนว่า: "การดำเนินการเขียนตอนนี้" ในเที่ยวบิน "มีการประมวลผลหลายเธรดหรือไม่ไม่มี" . ดังนั้นหากไม่มีเธรดการดำเนินการจะดำเนินการเองอย่างไรถ้าไม่อยู่ในเธรด
Yonatan Nir

6
นี่คือชิ้นส่วนที่ขาดหายไปในคำอธิบายนับพัน !!! มีบางคนกำลังทำงานอยู่เบื้องหลังพร้อมกับการดำเนินการ I / O ไม่ใช่เธรด แต่เป็นส่วนประกอบฮาร์ดแวร์เฉพาะสำหรับการทำงาน!
the_dark_destructor

2
@PrabuWeerasinghe: คอมไพเลอร์สร้าง struct ที่เก็บตัวแปรสถานะและท้องถิ่น หากการรอคอยต้องการที่จะให้ผลตอบแทน (เช่นกลับไปยังผู้เรียก) โครงสร้างนั้นจะถูกบรรจุไว้และใช้ชีวิตบนกอง
สตีเฟ่นเคลียร์

87

วิธีเดียวที่คอมพิวเตอร์สามารถทำสิ่งต่าง ๆ ได้มากกว่า 1 รายการในเวลาเดียวคือ (1) ทำสิ่งต่าง ๆ มากกว่า 1 รายการในเวลาเดียวกัน (2) จำลองโดยการกำหนดเวลางานและสลับไปมาระหว่างกัน ดังนั้นหากการรอคอยแบบ async ไม่ได้ทำเช่นนั้น

มันไม่ได้เป็นที่รอคอยไม่ค่าของคนเหล่านั้น โปรดจำไว้ว่าวัตถุประสงค์ของการawaitไม่ได้ที่จะทำให้รหัสของซิงโครตรงกันอย่างน่าอัศจรรย์ มันจะเปิดใช้งานโดยใช้เทคนิคเดียวกันกับที่เราใช้สำหรับการเขียนโค้ดซิงโครเมื่อเรียกเป็นรหัสไม่ตรงกัน รอเป็นเรื่องเกี่ยวกับการทำรหัสที่ใช้การดำเนินการเวลาแฝงที่สูงมีลักษณะเช่นรหัสที่ใช้การดำเนินงาน latency ต่ำ การดำเนินการเวลาแฝงสูงเหล่านั้นอาจอยู่ในเธรดอาจอยู่ในฮาร์ดแวร์วัตถุประสงค์พิเศษพวกเขาอาจฉีกงานของพวกเขาออกเป็นชิ้นเล็ก ๆ และวางไว้ในคิวข้อความเพื่อประมวลผลโดยเธรด UI ในภายหลัง พวกเขากำลังทำอะไรบางอย่างเพื่อให้บรรลุแบบอะซิงโครนัส แต่พวกเขาเป็นคนที่กำลังทำมัน รอเพียงแค่ให้คุณใช้ประโยชน์จากอะซิงโครนัสนั้น

นอกจากนี้ฉันคิดว่าคุณไม่มีตัวเลือกที่สาม พวกเราแก่เฒ่า - เด็ก ๆ วันนี้ด้วยเพลงแร็พของพวกเขาควรจะออกจากสนามหญ้าของฉัน ฯลฯ - จดจำโลกของ Windows ในช่วงต้นทศวรรษ 1990 ไม่มีเครื่องที่ใช้ CPU หลายตัวและไม่มีตัวตั้งเวลาเธรด คุณต้องการที่จะทำงานสองปพลิเคชันของ Windows ในเวลาเดียวกันคุณต้องให้ผลผลิต มัลติทาสกิ้งเป็นสหกรณ์ ระบบปฏิบัติการบอกกระบวนการที่จะทำงานและถ้ามันไม่ปฏิบัติก็จะหยุดกระบวนการอื่น ๆ ทั้งหมดจากการให้บริการ มันทำงานจนกว่ามันจะให้ผลผลิตและอย่างใดก็จะต้องรู้วิธีการรับที่มันออกไปในครั้งต่อไปที่มือระบบปฏิบัติการควบคุมกลับไปที่มัน. โค้ดอะซิงโครนัสแบบเธรดเดี่ยวมีจำนวนมากเช่นนั้นพร้อมกับ "คอย" แทน "อัตราผลตอบแทน" การรอหมายถึง "ฉันจะจำได้ว่าฉันออกไปจากที่นี่ที่ไหนและปล่อยให้คนอื่นวิ่งไปพักหนึ่งแล้วโทรกลับหาฉันเมื่องานที่ฉันรอเสร็จแล้วฉันจะไปรับที่ที่ฉันออกไป" ฉันคิดว่าคุณสามารถเห็นวิธีการที่ทำให้แอปตอบสนองได้ดีขึ้นเช่นเดียวกับใน Windows 3 วัน

การเรียกใช้วิธีการใด ๆ หมายถึงการรอให้วิธีการนั้นเสร็จสมบูรณ์

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

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


ภาษาระดับสูงอื่น ๆ ที่ทันสมัยสนับสนุนพฤติกรรมที่คล้ายกันอย่างชัดเจนเช่นกัน (เช่นฟังก์ชั่นทำบางสิ่งบางอย่างให้ผลตอบแทน [อาจส่งค่า / วัตถุบางอย่างไปยังผู้โทร] ดำเนินการต่อที่ที่เหลือเมื่อออกจากการควบคุม ) เครื่องกำเนิดไฟฟ้ามีขนาดใหญ่มากใน Python สำหรับสิ่งหนึ่ง
JAB

2
@JAB: แน่นอน เครื่องกำเนิดไฟฟ้าเรียกว่า "ตัววนซ้ำบล็อก" ใน C # และใช้yieldคำสำคัญ ทั้งasyncวิธีการและตัววนซ้ำใน C # เป็นรูปแบบของcoroutineซึ่งเป็นคำทั่วไปสำหรับฟังก์ชั่นที่รู้วิธีที่จะระงับการดำเนินงานปัจจุบันเพื่อเริ่มต้นใหม่ในภายหลัง จำนวนภาษามีการควบคุม coroutine หรือ coroutine เหมือนไหลวันนี้
Eric Lippert

1
การเปรียบเทียบเพื่อให้ได้ผลนั้นดีมาก - มันเป็นมัลติทาสก์แบบร่วมมือกันภายในกระบวนการเดียว (และหลีกเลี่ยงปัญหาเสถียรภาพของระบบของการทำงานมัลติทาสก์แบบร่วมมือกันทั่วทั้งระบบ)
253751

3
ฉันคิดว่าแนวคิดของ "cpu ขัดจังหวะ" ที่ใช้สำหรับ IO ไม่ทราบเกี่ยวกับโมเด็ม "โปรแกรมเมอร์" จำนวนมากดังนั้นพวกเขาคิดว่าเธรดต้องรอ IO แต่ละบิต
Ian Ringrose

@EricLippert Async วิธีการของ WebClient สร้างเธรดเพิ่มเติมจริง ๆ ดูที่นี่stackoverflow.com/questions/48366871/…
KevinBui

28

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

หลังจากการวิจัยของฉันเองในที่สุดฉันก็พบชิ้นส่วนที่ขาดหายไป: select(). โดยเฉพาะ IO มัลติดำเนินการโดยเมล็ดต่างๆภายใต้ชื่อที่แตกต่างกัน: select(), poll(), ,epoll() kqueue()สิ่งเหล่านี้คือการเรียกของระบบซึ่งในขณะที่รายละเอียดการใช้งานแตกต่างกันทำให้คุณสามารถส่งผ่านชุดของตัวอธิบายไฟล์เพื่อรับชม จากนั้นคุณสามารถโทรอีกครั้งที่บล็อกจนกว่าหนึ่งในไฟล์ descriptors ที่เฝ้าดูมีการเปลี่ยนแปลง

ดังนั้นหนึ่งสามารถรอชุดของเหตุการณ์ IO (ห่วงเหตุการณ์หลัก) จัดการเหตุการณ์แรกที่เสร็จสิ้นแล้วให้ผลตอบแทนการควบคุมกลับไปที่วงเหตุการณ์ ล้างและทำซ้ำ

มันทำงานอย่างไร คำตอบสั้น ๆ ก็คือเคอร์เนลและเวทย์มนตร์ระดับฮาร์ดแวร์ คอมพิวเตอร์มีส่วนประกอบหลายอย่างนอกเหนือจาก CPU และส่วนประกอบเหล่านี้สามารถทำงานแบบขนาน เคอร์เนลสามารถควบคุมอุปกรณ์เหล่านี้และสื่อสารโดยตรงกับพวกเขาเพื่อรับสัญญาณบางอย่าง

การเรียกใช้ระบบมัลติเพล็กซิ่ง IO เหล่านี้เป็นโครงสร้างพื้นฐานของลูปเหตุการณ์แบบเธรดเดียวเช่น node.js หรือ Tornado เมื่อคุณawaitฟังก์ชั่นคุณกำลังเฝ้าดูเหตุการณ์บางอย่าง (การทำให้ฟังก์ชั่นนั้นเสร็จสมบูรณ์) จากนั้นให้การควบคุมกลับไปที่ลูปเหตุการณ์หลัก เมื่อเหตุการณ์ที่คุณดูเสร็จสมบูรณ์ฟังก์ชั่น (ในที่สุด) จะรับจากตำแหน่งที่ค้างไว้ ฟังก์ชั่นที่ช่วยให้คุณที่จะระงับและการคำนวณประวัติการทำงานเช่นนี้จะเรียกว่าcoroutines


25

awaitและasyncใช้งานไม่ใช่เธรด

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

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

เนื่องจากasync/awaitใช้งาน s พวกเขาจะไม่สร้างเธรดใหม่


ในขณะที่เทคนิคการเขียนโปรแกรมขัดจังหวะใช้กันอย่างแพร่หลายในทุกระบบปฏิบัติการที่ทันสมัยฉันไม่คิดว่าพวกเขามีความเกี่ยวข้องที่นี่
คุณสามารถมีสองงาน CPU ผูกมัดการดำเนินการในแบบคู่ขนาน (บรรณนิทัศน์จริง) ใน CPU aysnc/awaitเดียวโดยใช้
ที่ไม่สามารถอธิบายได้เพียงกับความจริงที่ว่าสนับสนุนระบบปฏิบัติการเข้าคิวIORP


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

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

method:
   instr1                  
   instr2
   await task1
   instr3
   instr4
   await task2
   instr5
   return value

มันเปลี่ยนเป็นอะไรแบบนี้

int state = 0;

Task nextStep()
{
  switch (state)
  {
     case 0:
        instr1;
        instr2;
        state = 1;

        task1.addContinuation(nextStep());
        task1.start();

        return task1;

     case 1:
        instr3;
        instr4;
        state = 2;

        task2.addContinuation(nextStep());
        task2.start();

        return task2;

     case 2:
        instr5;
        state = 0;

        task3 = new Task();
        task3.setResult(value);
        task3.setCompleted();

        return task3;
   }
}

method:
   nextStep();

1 ที่จริงแล้วกลุ่มสามารถมีนโยบายการสร้างงานได้


16

ฉันจะไม่แข่งขันกับ Eric Lippert หรือ Lasse V. Karlsen และคนอื่น ๆ ฉันเพียงต้องการดึงความสนใจไปยังอีกแง่มุมของคำถามนี้ซึ่งฉันคิดว่าไม่ได้กล่าวถึงอย่างชัดเจน

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

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


3
มันไม่ใช่การแข่งขันในตอนแรก มันเป็นความร่วมมือ!
Eric Lippert

16

นี่คือวิธีที่ฉันดูทั้งหมดนี้มันอาจจะไม่แม่นยำในทางเทคนิค แต่มันช่วยฉันอย่างน้อย :)

โดยทั่วไปมีการประมวลผลสองประเภท (การคำนวณ) ที่เกิดขึ้นบนเครื่อง:

  • การประมวลผลที่เกิดขึ้นบน CPU
  • การประมวลผลที่เกิดขึ้นกับโปรเซสเซอร์อื่น ๆ (GPU การ์ดเครือข่าย ฯลฯ ) มาเรียกพวกมันว่า IO

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

ตัวอย่างบางส่วน:

  • ถ้าฉันใช้เมธอดเขียนของFileStreamวัตถุ (ซึ่งเป็นสตรีม) การประมวลผลจะพูดว่ามีการผูก CPU 1% และถูกผูกไว้ 99% IO
  • ถ้าฉันใช้เมธอดเขียนของNetworkStreamวัตถุ (ซึ่งเป็นสตรีม) การประมวลผลจะพูดว่ามีการผูก CPU 1% และถูกผูกไว้ 99% IO
  • ถ้าฉันใช้เมธอดเขียนของMemorystreamวัตถุ (ซึ่งเป็นสตรีม) การประมวลผลจะเป็น CPU 100% ที่ถูกผูกไว้

อย่างที่คุณเห็นจากมุมมองของโปรแกรมเมอร์เชิงวัตถุถึงแม้ว่าฉันจะเข้าถึงStreamวัตถุอยู่เสมอแต่สิ่งที่เกิดขึ้นด้านล่างอาจขึ้นอยู่กับประเภทของวัตถุ

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

ตัวอย่างบางส่วน:

  • ในแอปเดสก์ท็อปฉันต้องการพิมพ์เอกสาร แต่ไม่ต้องการรอ
  • เว็บเซิร์ฟเวอร์เซิร์ฟเวอร์ของฉันไคลเอนต์จำนวนมากในเวลาเดียวกันแต่ละคนได้รับเพจของเขาในแบบคู่ขนาน (ไม่ต่อเนื่องกัน)

ก่อนที่จะซิงค์ / รอคอยเรามีวิธีแก้ปัญหาสองประการดังนี้:

  • หัวข้อ มันค่อนข้างใช้งานง่ายด้วยคลาส Thread และ ThreadPool หัวข้อกำลังของ CPU ที่ถูกผูกไว้เท่านั้น
  • โมเดลการเขียนโปรแกรมแบบอะซิงโครนัส"เก่า" เริ่มต้น / สิ้นสุด / AsyncCallback มันเป็นแค่แบบจำลองมันไม่ได้บอกคุณว่าคุณจะใช้ CPU หรือ IO หากคุณดูที่คลาส Socket หรือ FileStream มันเป็น IO ที่ผูกมัดซึ่งก็เจ๋ง แต่เราไม่ค่อยใช้มัน

async / รอคอยเป็นเพียงรูปแบบการเขียนโปรแกรมร่วมกันบนพื้นฐานของแนวคิดงาน มันใช้งานได้ง่ายกว่าเธรดหรือเธรดพูลสำหรับงานที่ผูกกับ CPU และใช้งานได้ง่ายกว่ารุ่น Begin / End รุ่นเก่า Undercovers อย่างไรก็ตามมันเป็นเพียงแค่ห่อหุ้มคุณลักษณะที่ซับซ้อนสุดในทั้งสองอย่าง

ดังนั้นการชนะที่แท้จริงส่วนใหญ่จะเป็นงาน IO Boundงานที่ไม่ได้ใช้ CPU แต่ async / await ยังคงเป็นเพียงรูปแบบการเขียนโปรแกรมมันไม่ได้ช่วยให้คุณทราบว่าการประมวลผลจะเกิดขึ้นที่ใด

มันหมายความว่าไม่ใช่เพราะคลาสมีเมธอด "DoSomethingAsync" ที่ส่งคืนออบเจ็กต์งานที่คุณสามารถสันนิษฐานได้ว่ามันจะเป็น CPU ที่ถูกผูกไว้ (ซึ่งหมายความว่ามันอาจไร้ประโยชน์โดยเฉพาะอย่างยิ่งถ้ามันไม่มีพารามิเตอร์โทเค็นการยกเลิก) หรือ IO Bound (ซึ่งหมายความว่ามันอาจจะต้อง ) หรือการรวมกันของทั้งสองอย่าง (เนื่องจากแบบจำลองนั้นค่อนข้างมีความสัมพันธ์กับไวรัสการผูกมัดและผลประโยชน์ที่อาจเกิดขึ้นได้ในท้ายที่สุดจะมีความหลากหลายและไม่ชัดเจนมากนัก)

ดังนั้นกลับมาที่ตัวอย่างของฉันการดำเนินการเขียนของฉันโดยใช้ async / await บน MemoryStream จะคง CPU ไว้ (ฉันอาจจะไม่ได้ประโยชน์จากมัน) แม้ว่าฉันจะได้รับประโยชน์จากไฟล์และสตรีมเครือข่ายแน่นอน


1
นี่เป็นคำตอบที่ดีในการใช้ theadpool สำหรับ cpu bound งานในแง่ที่ว่าเธรด TP ควรถูกใช้เพื่อ offload การปฏิบัติการ IO imo ที่ทำงานกับ CPU ควรปิดกั้นด้วย caveats แน่นอนและไม่มีข้อ จำกัด ในการใช้งานหลายเธรด
davidcarr

3

สรุปคำตอบอื่น ๆ :

Async / await ถูกสร้างขึ้นเป็นหลักสำหรับงานที่ถูกผูกไว้ของ IO ตั้งแต่ใช้งานพวกเขาสามารถหลีกเลี่ยงการบล็อกเธรดการโทร การใช้งานหลักของพวกเขาคือกับเธรด UI ที่ไม่ต้องการให้เธรดถูกบล็อกในการดำเนินการที่ถูกผูกไว้กับ IO

Async ไม่ได้สร้างกระทู้ของตัวเอง กระทู้ของวิธีการโทรจะใช้ในการดำเนินการวิธีการ async จนกว่ามันจะหาที่รอ เธรดเดียวกันจะยังคงดำเนินการส่วนที่เหลือของวิธีการโทรที่นอกเหนือจากการเรียกวิธีการ async ภายในเมธอด async ที่เรียกว่าหลังจากกลับมาจากการรอคอยได้ความต่อเนื่องสามารถดำเนินการได้บนเธรดจากเธรดพูล - ตำแหน่งเดียวที่เธรดแยกต่างหากจะปรากฏในรูปภาพ


สรุปดี แต่ฉันคิดว่าควรตอบ 2 คำถามเพิ่มเติมเพื่อให้ภาพเต็ม: 1. เธรดใดที่รอรหัสถูกประมวลผลบน 2. ใครเป็นผู้ควบคุม / กำหนดค่าเธรดพูลที่กล่าวถึง - ผู้พัฒนาหรือสภาพแวดล้อมรันไทม์?
stojke

1. ในกรณีนี้ส่วนใหญ่รหัสที่รอคอยคือการดำเนินการที่ถูกผูกไว้กับ IO ซึ่งจะไม่ใช้เธรด CPU หากต้องการใช้การรอคอยสำหรับการทำงานที่เชื่อมโยงกับ CPU งานที่แยกต่างหากอาจเกิดขึ้นได้ 2. เธรดในเธรดพูลจัดการโดยตัวกำหนดเวลางานซึ่งเป็นส่วนหนึ่งของเฟรมเวิร์ก TPL
vaibhav kumar

2

ฉันพยายามอธิบายมันให้ลึกที่สุด บางทีบางคนอาจพบว่ามีประโยชน์ ฉันอยู่ที่นั่นทำสิ่งนั้นคิดค้นใหม่เมื่อทำเกมง่าย ๆ ใน DOS ใน Pascal (เก่าดี ... )

ดังนั้น ... ในทุก ๆ แอปพลิเคชั่นที่ขับเคลื่อนด้วยเหตุการณ์จะมีลูปของเหตุการณ์อยู่ภายในนั่นคือสิ่งนี้:

while (getMessage(out message)) // pseudo-code
{
   dispatchMessage(message); // pseudo-code
}

กรอบงานมักจะซ่อนรายละเอียดนี้จากคุณ แต่อยู่ที่นั่น ฟังก์ชัน getMessage จะอ่านเหตุการณ์ถัดไปจากคิวเหตุการณ์หรือรอจนกว่าเหตุการณ์จะเกิดขึ้น: การเลื่อนเมาส์, คีย์ดาวน์, คีย์อัป, คลิก, ฯลฯ จากนั้น dispatchMessage จะส่งเหตุการณ์ไปยังตัวจัดการเหตุการณ์ที่เหมาะสม จากนั้นรอเหตุการณ์ถัดไปและจนกว่าจะมีเหตุการณ์เลิกที่ออกจากลูปและจบแอปพลิเคชัน

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

void expensiveOperation()
{
    for (int i = 0; i < 1000; i++)
    {
        Thread.Sleep(10);
    }
}

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

ดังนั้นคุณจะเปลี่ยนสิ่งนี้เป็น:

void expensiveOperation()
{
    doIteration(0);
}

void doIteration(int i)
{
    if (i >= 1000) return;
    Thread.Sleep(10); // Do a piece of work.
    postFunctionCallMessage(() => {doIteration(i + 1);}); // Pseudo code. 
}

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

ตราบใดที่ภารกิจที่รันอยู่นานกำลังรันเหตุการณ์ต่อเนื่องจะอยู่ในคิวเหตุการณ์เสมอ ดังนั้นคุณจึงคิดค้นตารางงานของคุณเอง โดยที่เหตุการณ์ความต่อเนื่องในคิวคือ "กระบวนการ" ที่กำลังทำงานอยู่ ที่จริงแล้วสิ่งที่ระบบปฏิบัติการทำยกเว้นว่าการส่งเหตุการณ์ต่อเนื่องและกลับไปที่ลูปตัวกำหนดเวลาจะทำผ่านตัวจับเวลาของ CPU ซึ่งระบบปฏิบัติการลงทะเบียนรหัสการสลับบริบทดังนั้นคุณจึงไม่จำเป็นต้องสนใจมัน แต่ที่นี่คุณกำลังเขียนตารางเวลาของคุณเองดังนั้นคุณจึงต้องใส่ใจมัน - จนถึงตอนนี้

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

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


0

จริงๆแล้วasync awaitChains เป็นเครื่องสถานะที่สร้างโดยคอมไพเลอร์ CLR

async await อย่างไรก็ตามจะใช้เธรดที่ TPL กำลังใช้เธรดพูลเพื่อดำเนินงาน

เหตุผลที่แอ็พพลิเคชันไม่ถูกบล็อกคือเครื่องสถานะสามารถตัดสินใจได้ว่า co-กิจวัตรใดที่จะดำเนินการทำซ้ำตรวจสอบและตัดสินใจอีกครั้ง

อ่านเพิ่มเติม:

async และรอสร้างอะไร

Async รอและเครื่องสร้างรัฐ

อะซิงโครนัส C # และ F # (III.): มันทำงานอย่างไร - Tomas Petricek

แก้ไข :

ตกลง. ดูเหมือนว่ารายละเอียดของฉันไม่ถูกต้อง อย่างไรก็ตามฉันต้องชี้ให้เห็นว่าเครื่องจักรของรัฐเป็นสินทรัพย์ที่สำคัญสำหรับasync awaits แม้ว่าคุณจะรับ I / O อะซิงโครนัสคุณยังต้องมีผู้ช่วยในการตรวจสอบว่าการดำเนินการเสร็จสมบูรณ์แล้วดังนั้นเรายังคงต้องการเครื่องสถานะและกำหนดกิจวัตรที่สามารถดำเนินการร่วมกันได้


0

นี่ไม่ใช่การตอบคำถามโดยตรง แต่ฉันคิดว่ามันเป็นข้อมูลเพิ่มเติมที่ขัดขวาง:

Async และ Rossit ไม่ได้สร้างเธรดใหม่ แต่ขึ้นอยู่กับตำแหน่งที่คุณใช้ async ที่รอส่วนซิงโครนัสก่อนที่ await อาจทำงานบนเธรดที่แตกต่างจากส่วนซิงโครรูสหลังจากรอ (ตัวอย่างเช่น ASP.NET และ ASP.NET หลักแตกต่างกัน)

ในแอปพลิเคชันที่ใช้ UI-Thread (WinForms, WPF) คุณจะอยู่ในเธรดเดียวกันก่อนและหลัง แต่เมื่อคุณใช้ async ในเธรดพูลเธรดเธรดก่อนและหลังการรออาจไม่เหมือนกัน

วิดีโอยอดเยี่ยมในหัวข้อนี้

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