8.2. คอรูทีนและงาน¶
คอรูทีนคือหน่วยงานที่โปรแกรม asyncio ใช้สร้าง ส่วนงาน (task) คือวิธีที่แอปพลิเคชันรันคอรูทีนหลายตัวพร้อมกัน
8.2.1. คอรูทีน¶
คอรูทีน คือฟังก์ชันที่ประกาศด้วย async def
import asyncio
async def heartbeat(interval_ms):
while True:
print("tick")
await asyncio.sleep_ms(interval_ms)
โครงสร้างของเนื้อหาคล้ายกับฟังก์ชันทั่วไป แต่มีส่วนประกอบพิเศษเพิ่มเติม คือ await ทุกครั้งที่คอรูทีนต้องรอบางสิ่ง ไม่ว่าจะเป็นการนอนหลับ การอ่านข้อมูลจากเครือข่าย หรือการรอเหตุการณ์ที่ถูกตั้งค่า มันจะ awaitออกนิพจน์ที่รู้วิธีระงับคอรูทีนจนกว่าสิ่งที่รออยู่จะพร้อม ที่ทุก await คอรูทีนจะส่งคืนการควบคุมกลับไปยัง asyncio และ asyncio จะเรซูมจากจุดเดิมเมื่อการดำเนินการที่รออยู่เสร็จสิ้น
โมดูล asyncio มาพร้อมกับการนอนหลับสองแบบ:
asyncio.sleep()-- อาร์กิวเมนต์เป็นวินาที รับค่าทศนิยมasyncio.sleep_ms()-- อาร์กิวเมนต์เป็นมิลลิวินาที รับค่าจำนวนเต็ม เป็นส่วนขยายของ MicroPython และมักเป็นตัวเลือกที่เหมาะสมบนกล้อง เนื่องจากการตั้งค่าเวลาใน เฟิร์มแวร์ใช้หน่วยมิลลิวินาที
async def เปล่าๆ ไม่ทำ อะไร ด้วยตัวเอง การเรียก heartbeat(500) ไม่ได้รันเนื้อหา แต่จะคืนค่า อ็อบเจกต์ คอรูทีนที่ asyncio ต้องจัดตาราง วิธีที่ง่ายที่สุดในการจัดตารางคือ asyncio.run()
asyncio.run(heartbeat(500))
asyncio.run() เริ่มต้น event loop จัดตารางคอรูทีนที่ได้รับเป็นจุดเริ่มต้นระดับบนสุด ขับเคลื่อน loop จนกว่าคอรูทีนนั้นจะคืนค่า แล้วปิด loop ลง สำหรับคอรูทีนเดียว นั่นคือโปรแกรมทั้งหมด สำหรับคอรูทีนหลายตัว แอปพลิเคชันจะใช้งาน (task)
8.2.2. งาน¶
งาน คือตัวครอบคอรูทีนของ asyncio ที่บอกว่า จัดตารางนี้พร้อมกันกับตัวปัจจุบันและให้ฉันทำงานต่อไป asyncio.create_task() สร้างหนึ่งตัวและ คืนค่า อ็อบเจกต์ Task ที่แทนงานที่ถูกจัดตาราง:
task = asyncio.create_task(heartbeat("fast", 100))
ขณะนี้คอรูทีนอยู่ในตารางของ loop แล้ว ผู้เรียกยังไม่ได้รอมัน Task ที่คืนค่ามาคือตัวจัดการที่ผู้เรียกใช้ภายหลังเพื่อโต้ตอบกับงานที่กำลังทำงานนั้น
เมื่อแอปพลิเคชันมีตัวจัดการแล้ว มันสามารถทำสามสิ่งด้วย:
รอให้งานเสร็จสิ้น
Taskนั้น await ได้ด้วยตัวเองresult = await taskระงับคอรูทีนปัจจุบันจนกว่าคอรูทีนของtaskจะคืนค่า แล้วเรซูมพร้อมกับค่าที่คอรูทีนนั้นคืน (หรือยกข้อยกเว้นที่มันยกขึ้นมาอีกครั้ง)ยกเลิกงาน
task.cancel()จัดตารางasyncio.CancelledErrorให้ถูกยกขึ้นภายในคอรูทีนของงานที่awaitครั้งถัดไป ให้โอกาสมันรันโค้ดทำความสะอาดในบล็อกfinallyหน้าเกี่ยวกับ timeouts and cancellation ครอบคลุมรายละเอียดระบุมันภายหลัง
asyncio.current_task()คืนค่าTaskสำหรับคอรูทีนที่กำลังทำงานอยู่ในขณะนั้น สคริปต์ส่วนใหญ่ไม่เคยเรียกมัน มันปรากฏในเครื่องมือวัดและในตัวจัดการข้อยกเว้น
สคริปต์ไม่จำเป็นต้องเก็บตัวจัดการทุกครั้ง งานพื้นหลังที่แอปพลิเคชันเริ่มและปล่อยให้ทำงานอยู่สามารถละทิ้งค่าที่คืนมาได้ loop ยังคงจัดตารางให้:
import asyncio
async def heartbeat(name, interval_ms):
while True:
print(name)
await asyncio.sleep_ms(interval_ms)
async def main():
asyncio.create_task(heartbeat("fast", 100))
asyncio.create_task(heartbeat("slow", 500))
await asyncio.sleep(5)
asyncio.run(main())
การเรียก create_task สองครั้งจัดตาราง heartbeat ทั้งสองโดยไม่รอตัวใดตัวหนึ่ง การควบคุมส่งคืนทันทีไปยัง main ซึ่งจากนั้น awaitรอการนอนหลับห้าวินาที ระหว่างที่มันนอน งาน heartbeat ทั้งสองก้าวหน้า loop หมุนผ่านงานใดก็ตามที่พร้อมทำงาน หลังจากห้าวินาที main คืนค่า loop ปิดงานที่ยังทำงานอยู่ทั้งหมด และ asyncio.run() คืนค่าไปยังผู้เรียก
เก็บตัวจัดการเมื่อแอปพลิเคชันต้องการหนึ่งในสามการดำเนินการข้างต้นจริงๆ ในทางปฏิบัติหมายถึง เกือบทุกครั้ง เนื่องจากการปิดแอปพลิเคชันอย่างถูกต้องหมายถึงการยกเลิกงานพื้นหลังที่มันสร้างขึ้น หน้าการยกเลิกครอบคลุมรูปแบบนั้น
8.2.3. กฎสองบรรทัด¶
โปรแกรม asyncio ขั้นต่ำคือ สองบรรทัด ที่ตัวอย่างข้างต้นจบด้วย:
async def main():
...
asyncio.run(main())
ทุกอย่างอื่น ไม่ว่าจะเป็นงานที่แอปพลิเคชันสร้าง primitives ที่ใช้ประสานงาน หรือสตรีมที่เปิด ล้วนเกิดขึ้น ภายใน main (และภายในคอรูทีนที่ main สร้าง) เมื่อสคริปต์โตเกินลูป while True: csi0.snapshot() แบบคลาสสิกของกล้อง คำตอบไม่ใช่การเรียก asyncio.run() หลายครั้ง แต่คือการพับงานใหม่เข้าไปใน main เป็นงานเพิ่มเติม