บทที่ 6.3: Asyncio และ Event Loop (การเขียนโปรแกรม Asynchronous ด้วย async / await)
เอกสารนี้อธิบายถึงแนวคิดการเขียนโปรแกรมแบบไม่รอชะงัก (Asynchronous Programming) การทำงานของ Event Loop การประกาศ Coroutine ด้วย async def การรอรับผลลัพธ์ด้วย await และการรันหลาย Task พร้อมกันด้วยโมดูล asyncio
1. แนวคิด Asynchronous Programming และ Event Loop
การเขียนโปรแกรมแบบ Asynchronous คือการบริหารจัดการให้โปรแกรมสามารถสลับไปทำงานอื่นได้ในขณะที่กำลังรอการทำงานบางอย่างอยู่ (เช่น รอคำตอบจาก Network API หรือการอ่าน/เขียน Disk) โดยใช้ทรัพยากรเพียง 1 Thread / 1 Process
Event Loop คืออะไร
Event Loop ทำหน้าที่เหมือนศูนย์ควบคุมกลางที่คอยบริหารจัดการคิวการทำงาน (Task Queue) เมื่อ Task ใดเกิดการรอ (I/O Wait) Event Loop จะสลับไปรัน Task อื่นที่พร้อมทำงานทันที ทำให้ไม่เสียเวลาในการรอ
[ Event Loop ]
│
├───> Task A (กำลังทำงาน) ───> ติดรอ I/O (สลับไปรัน Task B)
│
├───> Task B (ทำงานระหว่างรอ A)
│
└───> Task A (I/O เสร็จแล้ว ───> สลับกลับมาทำต่อ)
2. โครงสร้าง async และ await
async def: คำสั่งใช้นำหน้าฟังก์ชันเพื่อเปลี่ยนให้ฟังก์ชันนั้นเป็น Coroutineawait: คำสั่งใช้หยุดการทำงานของ Coroutine นั้นชั่วคราว เพื่อคืนสิทธิ์ให้ Event Loop ไปสลับรัน Task อื่น จนกว่าผลลัพธ์จะพร้อม
import asyncio
# สร้าง Coroutine Function
async def main():
print("เริ่มต้นการทำงาน")
# ใช้ asyncio.sleep แทน time.sleep เพื่อไม่ให้บล็อก Event Loop
await asyncio.sleep(1)
print("สิ้นสุดการทำงานหลังผ่านไป 1 วินาที")
# จุดเริ่มต้นรัน Event Loop (Python 3.7+)
asyncio.run(main())
3. การรันหลาย Tasks พร้อมกันด้วย asyncio.gather
หากมีหลาย Coroutine ที่ต้องการสั่งประมวลผลพร้อมกัน สามารถใช้ asyncio.gather() เพื่อสั่งให้รันแบบ Concurrent และรอรับผลลัพธ์กลับมาพร้อมกันได้
import asyncio
import time
async def fetch_data(source_id, delay):
print(f"กำลังดึงข้อมูลจากแหล่งที่ {source_id}...")
await asyncio.sleep(delay) # จำลองเวลาที่ใช้ในการดึงข้อมูล
print(f"ดึงข้อมูลจากแหล่งที่ {source_id} สำเร็จ")
return {"id": source_id, "status": "200 OK"}
async def main():
start_time = time.time()
# สั่งรัน 3 Tasks แบบขนานกันบน Event Loop เดียวกัน
results = await asyncio.gather(
fetch_data(1, 2),
fetch_data(2, 3),
fetch_data(3, 1)
)
print("\nผลลัพธ์ทั้งหมด:", results)
print(f"ใช้เวลารวมทั้งหมด: {time.time() - start_time:.2f} วินาที")
asyncio.run(main())
# Output:
# กำลังดึงข้อมูลจากแหล่งที่ 1...
# กำลังดึงข้อมูลจากแหล่งที่ 2...
# กำลังดึงข้อมูลจากแหล่งที่ 3...
# ดึงข้อมูลจากแหล่งที่ 3 สำเร็จ
# ดึงข้อมูลจากแหล่งที่ 1 สำเร็จ
# ดึงข้อมูลจากแหล่งที่ 2 สำเร็จ
#
# ผลลัพธ์ทั้งหมด: [{'id': 1, 'status': '200 OK'}, {'id': 2, 'status': '200 OK'}, {'id': 3, 'status': '200 OK'}]
# ใช้เวลารวมทั้งหมด: 3.00 วินาที (รันพร้อมกัน จึงใช้เวลาตาม Task ที่นานที่สุด)
4. การจัดการ Task ด้วย asyncio.create_task
กรณีต้องการส่ง Task เข้าไปทำงานใน Background ทันทีโดยยังไม่จำเป็นต้องสั่ง await ณ บรรทัดนั้น สามารถสร้าง Task ด้วย asyncio.create_task() ได้
import asyncio
async def background_worker(name, delay):
await asyncio.sleep(delay)
print(f"Task '{name}' ทำงานเสร็จสิ้น")
async def main():
# สร้าง Task ให้เริ่มทำงานทันทีใน Background
task1 = asyncio.create_task(background_worker("A", 2))
task2 = asyncio.create_task(background_worker("B", 1))
print("ทำโค้ดส่วนอื่นต่อระหว่างรอ Background Tasks...")
# สั่ง await เมื่อต้องการผลลัพธ์จาก Task นั้น
await task1
await task2
asyncio.run(main())
5. ข้อควรระวังในการเขียน Async Code
- อย่าใช้ Blocking Code ใน Async Function: ห้ามใช้ฟังก์ชันที่ทำงานแบบ Synchronous บล็อกการทำงาน เช่น
time.sleep(),requests.get()หรือการอ่านไฟล์ขนาดใหญ่แบบปกติ เพราะจะทำให้ Event Loop หยุดหยุดชะงัก ให้ใช้ Library ที่รองรับ Async โดยเฉพาะ เช่นaiohttp,httpxหรือaiofiles - ต้องเรียกด้วย
awaitหรือasyncio.run()เท่านั้น: การสั่งเรียกฟังก์ชันasync defโดยไม่ใช้awaitจะคืนค่ากลับมาเป็น Coroutine Object และโค้ดภายในจะไม่ถูกประมวลผลจริง
6. เปรียบเทียบ Concurrency Models ใน Python
| รูปแบบ | กลไกหลัก | เหมาะสำหรับงาน | ข้อดี |
|---|---|---|---|
| Threading | OS Threads | I/O-Bound | แชร์ Memory ง่าย ไม่ต้องแก้โครงสร้างโค้ดเดิมมาก |
| Multiprocessing | OS Processes | CPU-Bound | หลบเลี่ยง GIL ใช้ CPU ได้เต็มประสิทธิภาพทุก Core |
| Asyncio | Single-thread Event Loop | High-concurrency I/O | ประหยัด Memory สูงสุด รองรับ Connection ได้หลักหมื่น |