Skip to content

บทที่ 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: คำสั่งใช้นำหน้าฟังก์ชันเพื่อเปลี่ยนให้ฟังก์ชันนั้นเป็น Coroutine
  • await: คำสั่งใช้หยุดการทำงานของ 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

  1. อย่าใช้ Blocking Code ใน Async Function: ห้ามใช้ฟังก์ชันที่ทำงานแบบ Synchronous บล็อกการทำงาน เช่น time.sleep(), requests.get() หรือการอ่านไฟล์ขนาดใหญ่แบบปกติ เพราะจะทำให้ Event Loop หยุดหยุดชะงัก ให้ใช้ Library ที่รองรับ Async โดยเฉพาะ เช่น aiohttp, httpx หรือ aiofiles
  2. ต้องเรียกด้วย 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 ได้หลักหมื่น