CƠ CHẾ TIMEOUT, HỦY TÁC VỤ (CANCELLATION) & CẦU NỐI VỚI BLOCKING CODE

Sau khi đã nắm vững cách Event Loop điều phối các Task, ở buổi này chúng ta sẽ trang bị các kỹ năng phòng vệ tối quan trọng cho backend:

  1. Kiểm soát Timeout: Không để hệ thống bị treo vô tận khi server bên ngoài phản hồi chậm.

  2. Cơ chế Hủy tác vụ (task.cancel()): Thu hồi tài nguyên gọn gàng khi người dùng ngắt kết nối hoặc tác vụ không còn cần thiết.

  3. Cầu nối Blocking Code: Chạy các hàm đồng bộ nặng (CPU-bound hoặc thư viện legacy) mà không làm nghẽn Event Loop bằng asyncio.to_thread().

PHẦN 1: KIỂM SOÁT THỜI GIAN CHỜ VỚI asyncio.wait_for

Trong thực tế, một API bên thứ ba hoặc một câu truy vấn mạng có thể bị treo (hang) hàng chục phút nếu mạng đứt ngầm. Để bảo vệ hệ thống, ta dùng asyncio.wait_for().

Cú pháp:

try:
    result = await asyncio.wait_for(coroutine_or_task, timeout=so_giay)
except asyncio.TimeoutError:
    # Xử lý khi quá thời gian cho phép
  • Nếu tác vụ hoàn thành trước thời hạn: Trả về kết quả bình thường.

  • Nếu vượt quá timeout: Python tự động gửi tín hiệu hủy (cancel) tác vụ đó và ném ra ngoại lệ asyncio.TimeoutError.

PHẦN 2: BẢN CHẤT CỦA CƠ CHẾ HỦY TÁC VỤ (task.cancel())

Khi một Task đang chạy ngầm mà ta không cần nữa (ví dụ: client đóng tab trình duyệt, ngắt kết nối WebSocket):

  1. Gọi task.cancel(): Event Loop sẽ ném một ngoại lệ đặc biệt tên là asyncio.CancelledError ngay tại điểm await mà task đang tạm dừng.

  2. Nếu task không bắt lỗi này, nó sẽ dừng thực thi ngay lập tức.

  3. Dọn dẹp an toàn (Graceful Cleanup): Bọc khối lệnh trong try...finally để đảm bảo dù bị hủy ngang xương, các tài nguyên (file, socket, connection) vẫn được đóng sạch sẽ.

import asyncio

async def background_worker():
    try:
        print("Worker: Đang thực hiện sao lưu dữ liệu...")
        await asyncio.sleep(10) # Giả lập tác vụ chạy 10 giây
        print("Worker: Hoàn thành sao lưu!")
    except asyncio.CancelledError:
        print("⚠️ Worker: Nhận tín hiệu HỦY! Bắt đầu dọn dẹp file tạm...")
        # Thực hiện đóng kết nối / xóa file dở dang
        raise  # Bắt buộc re-raise ngoại lệ để Event Loop ghi nhận task đã hủy thành công
    finally:
        print("🔒 Worker: Hoàn tất dọn dẹp tài nguyên an toàn.")

async def main():
    task = asyncio.create_task(background_worker())
    
    # Để task chạy thử trong 2 giây rồi quyết định hủy
    await asyncio.sleep(2)
    print("Main: Quá thời gian, yêu cầu HỦY task!")
    task.cancel()

    try:
        await task
    except asyncio.CancelledError:
        print("Main: Đã xác nhận task bị hủy bỏ an toàn.")

asyncio.run(main())

PHẦN 3: CẦU NỐI BLOCKING CODE VỚI asyncio.to_thread()

Một lỗi kinh điển khi mới làm quen với bất đồng bộ là gọi một hàm đồng bộ nặng bên trong coroutine (ví dụ: time.sleep(), hàm băm mật khẩu bcrypt, xử lý ảnh với PIL/OpenCV, hoặc gọi thư viện đồng bộ requests). Khi đó, toàn bộ Event Loop bị đóng băng, các client khác đều bị nghẽn!

Từ Python 3.9+, Python cung cấp giải pháp chuẩn mực: asyncio.to_thread().

Plaintext
[ Luồng chính (Main Thread): Chạy Event Loop ]
      |
      | Gặp hàm nặng -> Đẩy sang ThreadPool ngầm của HĐH
      V
[ ThreadPool Worker (OS Thread) ] ----> Chạy xong báo kết quả ngược lại Event Loop

So sánh trực quan:

  • Cách viết SAI (Làm sập hiệu năng Event Loop):

import time
async def bad_handler():
    time.sleep(5) # LÀM ĐÓNG BĂNG MỌI TÁC VỤ KHÁC TRÊN SERVER!

Cách viết ĐÚNG (Non-blocking):

import time
import asyncio
def cpu_heavy_work(n):
    time.sleep(5) # Hoặc tính toán ma trận nặng
    return n * 2

async def good_handler():
    # Đẩy sang thread riêng, Event Loop vẫn tự do phục vụ client khác
    result = await asyncio.to_thread(cpu_heavy_work, 10)
    return result

PHẦN 4: MÃ NGUỒN TỔNG HỢP THỰC CHIẾN

Tạo file buoi3_timeout_and_threads.py tích hợp cả 3 kỹ thuật:

import asyncio
import time

def sync_heavy_task(duration: int) -> str:
    """Giả lập một hàm tính toán CPU/đồng bộ nặng mất nhiều thời gian"""
    print(f"  [Thread] Bắt đầu xử lý tác vụ nặng trong {duration}s...")
    time.sleep(duration)
    print(f"  [Thread] Hoàn tất xử lý tác vụ nặng!")
    return f"Dữ liệu đã xử lý ({duration}s)"

async def network_request_simulator(delay: int) -> str:
    """Giả lập một cuộc gọi API qua mạng"""
    try:
        print(f"🌐 [Network] Bắt đầu gọi API (dự kiến {delay}s)...")
        await asyncio.sleep(delay)
        return "Kết quả trả về từ API thành công!"
    except asyncio.CancelledError:
        print("⚠️ [Network] Tác vụ mạng bị hủy giữa chừng!")
        raise

async def main():
    print("=== 1. THỬ NGHIỆM TIMEOUT VỚI ASYNCIO.WAIT_FOR ===")
    try:
        # Gọi API dự kiến mất 5s, nhưng timeout chỉ cho phép 2s
        result = await asyncio.wait_for(network_request_simulator(delay=5), timeout=2.0)
        print("Kết quả:", result)
    except asyncio.TimeoutError:
        print("❌ Lỗi: Quá thời gian chờ (Timeout 2.0s)! Hệ thống đã tự hủy tác vụ mạng.")

    print("\n=== 2. THỬ NGHIỆM CHẠY HÀM BLOCKING VỚI ASYNCIO.TO_THREAD ===")
    start_time = time.time()

    # Trong khi hàm blocking đang chạy trên thread ngầm, Event Loop vẫn có thể chạy tác vụ khác
    task_heavy = asyncio.to_thread(sync_heavy_task, duration=3)
    
    # Một coroutine khác chạy song song trên Event Loop
    async def ticker():
        for i in range(1, 4):
            await asyncio.sleep(1)
            print(f"  ⏱️ [EventLoop] Vẫn đang hoạt động mượt mà... tick {i}s")

    # Chạy đồng thời cả 2 tác vụ
    results = await asyncio.gather(task_heavy, ticker())
    
    print(f"Kết quả nhận được: {results[0]}")
    print(f"⏱️ Tổng thời gian: {time.time() - start_time:.2f}s")

if __name__ == "__main__":
    asyncio.run(main())

PHẦN 5: BÀI TẬP SUY LUẬN CHO BÉ

  1. Thử nghiệm quan sát:

    • Trong phần 2 của code trên, thông báo ⏱️ [EventLoop] Vẫn đang hoạt động mượt mà... tick 1s, 2s, 3s có xuất hiện xen kẽ trong lúc hàm sync_heavy_task đang chạy không?

    • Điều này chứng minh Event Loop có bị hàm time.sleep(3) làm đông cứng không?

  2. Thử thách nâng cao:

    • Nếu ta dùng asyncio.wait_for(asyncio.to_thread(sync_heavy_task, 10), timeout=2.0) thì khi hết 2 giây, thread chạy hàm time.sleep(10) ngầm bên dưới của hệ điều hành có bị dừng ngay lập tức được không? (Gợi ý: Hệ điều hành có cho phép Python hủy ngang một thread đang chạy code C/blocking không?)