8.1. การทำงานพร้อมกันแบบร่วมมือ

โมเดลการจัดตารางของ asyncio เป็นแบบ cooperative ไม่ใช่ preemptive ความแตกต่างนี้คือโมเดลทางความคิดที่สำคัญที่สุดซึ่งส่วนที่เหลือของหัวข้อนี้สร้างขึ้น จึงคุ้มค่าที่จะทำความเข้าใจก่อนที่จะมีโค้ดใดๆ

8.1.1. Preemptive เทียบกับ cooperative

ตัวจัดตาราง preemptive — แบบที่ระบบปฏิบัติการเดสก์ท็อปใช้เพื่อให้โปรแกรมหลายโปรแกรมทำงานพร้อมกัน — สามารถหยุดโค้ดที่กำลังทำงานอยู่ได้ ทุกขณะ และสลับไปอีกโปรแกรมหนึ่ง โค้ดที่กำลังทำงานไม่จำเป็นต้องทำอะไรพิเศษ ตัวจัดตารางจะขัดจังหวะเอง ทำให้ preemptive scheduling มีความยืดหยุ่นมาก (ไม่มีโค้ดชิ้นใดทำให้ชิ้นอื่นไม่ได้ทำงานเพราะช้า) แต่ก็หมายความว่าตัวแปรที่ใช้ร่วมกันต้องได้รับการป้องกันอย่างระมัดระวัง เพราะการสลับอาจเกิดขึ้นได้ทุกที่ — แม้กระทั่งระหว่างการเขียนค่าหรือระหว่างการอ่าน list

ตัวจัดตาราง cooperative สามารถสลับระหว่างโค้ดได้เฉพาะที่จุดที่โค้ดที่กำลังทำงาน ส่งคืนการควบคุมอย่างชัดเจน บน asyncio จุดเหล่านั้นคือทุก await และทุกการเรียก coroutine ที่ yield ภายใน (ส่วนใหญ่คือ asyncio.sleep()) ระหว่างสอง await coroutine ที่กำลังทำงานมี CPU เป็นของตัวเอง

ผลลัพธ์สองประการที่ตามมาคือ:

  • coroutine ที่ไม่เคย await จะไม่ถูกหยุดชั่วคราว หาก coroutine อยู่ใน tight loop โดยไม่มี await ภายใน มันจะผูกขาด scheduler และไม่มีสิ่งอื่นใดคืบหน้า การแก้ไขคือ await asyncio.sleep_ms(0) (หรือการเรียกรอแบบอื่น) ที่จุดที่เหมาะสมใน loop

  • สถานะที่ใช้ร่วมกันปลอดภัยระหว่าง await สอง coroutine ไม่สามารถสลับกันกลางการดำเนินการที่ไม่มี await ได้ การเสียหายแบบที่เกิดขึ้นเมื่อ preemption ตกกลางการอัปเดตหลายขั้นตอน — โค้ดชิ้นหนึ่งอ่านค่าขณะที่อีกชิ้นกำลังเปลี่ยนแปลง — ไม่สามารถเกิดขึ้นที่นี่ได้เลย การประสานงานระหว่าง coroutine ยังคงจำเป็นเมื่อหลาย coroutine ต้องใช้ทรัพยากรร่วมกัน ข้าม await แต่ปัญหาการสลับกันกลางบรรทัดไม่มีผลที่นี่

8.1.2. สามชั้น

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

  • Coroutines — ฟังก์ชันที่ประกาศด้วย async def แต่ละตัวเป็นหน่วยงานที่ดูแลตัวเองและ await ตามความเหมาะสม Python Overview แนะนำคีย์เวิร์ด async/await แล้ว ใน asyncio นี่คือวิธีที่ coroutine ส่งคืนการควบคุมกลับไปยัง scheduler

  • Tasks — wrapper ที่ asyncio.create_task() ห่อรอบ coroutine เพื่อ จัดตารางให้ทำงานพร้อมกันกับ coroutine ปัจจุบัน แอปพลิเคชันมักสร้าง task จำนวนหนึ่งสำหรับงานที่ทำงานต่อเนื่อง (loop สแนปช็อต, network client, UART reader, ...)

  • Event loop — เอ็นจิ้นที่อยู่เบื้องหลังซึ่งติดตามว่า coroutine ใดกำลังรอและ coroutine ใดพร้อมทำงาน โดยสลับระหว่าง task ในทุก await แอปพลิเคชันไม่ต้องเขียน loop เอง แต่ส่ง coroutine ระดับสูงสุดไปยัง asyncio.run() และ loop จะขับเคลื่อนทุกอย่างจากที่นั่น

เมื่ออธิบายแอปพลิเคชันในลักษณะนั้น — เป็นชุด coroutine เล็กๆ ที่ประกอบกันโดย event loop — การทำงานพร้อมกันจะกลายเป็นคุณสมบัติของโครงสร้างโปรแกรม ไม่ใช่สิ่งที่แอปพลิเคชันต้องจัดการทีละขั้นตอน