ĐIỀU HƯỚNG LUỒNG SỰ KIỆN VỚI asyncio.Event & asyncio.Condition

Nếu như ở Module 1, các Task hoạt động khá độc lập (task nào biết task nấy), thì trong các hệ thống thực tế (Backend Web, Game Server, IoT), các tác vụ phải nói chuyện và phối hợp nhịp nhàng với nhau.

Ví dụ:

  • Máy chủ Web phải chờ kết nối Database thành công thì mới cho phép các API xử lý đơn hàng.

  • Khi mất mạng, toàn bộ các luồng gửi tin nhắn phải tạm dừng xếp hàng chờ, khi mạng có lại thì đồng loạt chạy tiếp.

Để làm được điều này mà không dùng vòng lặp while True: await asyncio.sleep(...) tốn CPU, Python cung cấp hai công cụ điều hướng luồng chuẩn mực: asyncio.Event và asyncio.Condition.

PHẦN 1: asyncio.Event – CỜ BÁO HIỆU TRẠNG THÁI (BOOLEAN FLAG)

1. Bản chất hoạt động

asyncio.Event quản lý một lá cờ nội bộ (Internal Flag) chỉ có 2 trạng thái: ĐÃ DỰNG CỜ (True) hoặc ĐANG HẠ CỜ (False). Ban đầu khi mới khởi tạo, cờ luôn ở trạng thái False.

Cơ chế chỉ xoay quanh 4 phương thức cốt lõi:

  • await event.wait(): Tác vụ đứng yên tại đây chờ đợi. Nếu cờ đang False, nó tạm dừng và nhường CPU cho việc khác. Ngay khi cờ chuyển sang True, nó được đánh thức và chạy tiếp ngay lập tức!

  • event.set(): Dựng cờ lên (True). Tất cả các tác vụ đang wait() sẽ đồng loạt được đánh thức cùng lúc.

  • event.clear(): Hạ cờ xuống (False). Các tác vụ đến sau gọi wait() sẽ tiếp tục phải đứng chờ.

  • event.is_set(): Trả về trạng thái cờ hiện tại (True hoặc False).

Plaintext
                  [ Tác vụ A: await event.wait() ] ---\
                  [ Tác vụ B: await event.wait() ] ----+---> (Đang ngủ say chờ cờ)
                  [ Tác vụ C: await event.wait() ] ---/
                                 ^
                                 | event.set() (Phát còi báo hiệu!)
                                 |
                 [ Tác vụ Khởi tạo Hệ thống / Admin ]

2. Thực hành 1: Mô hình Khởi động Hệ thống (Startup Gatekeeper)

Tạo file buoi4_event_gatekeeper.py:

import asyncio
import time

# Tạo đối tượng cờ hiệu dùng chung
db_ready_event = asyncio.Event()

async def database_initializer():
    """Tác vụ khởi tạo: Giả lập nạp cấu hình và kết nối DB mất 3 giây"""
    print("⏳ [DB] Bắt đầu kết nối Database và nạp cấu hình hệ thống...")
    await asyncio.sleep(3)
    print("✅ [DB] Database đã sẵn sàng 100%!")
    
    # Bật cờ hiệu cho toàn bộ hệ thống biết
    db_ready_event.set()

async def worker(worker_id: int):
    """Tác vụ xử lý: Cần DB sẵn sàng mới được phép nhận việc"""
    print(f"  Worker-{worker_id}: Đang chờ tín hiệu Database...")
    
    # Đứng chờ tại đây, không tốn chút tài nguyên CPU nào
    await db_ready_event.wait()
    
    print(f"🚀 Worker-{worker_id}: Đã nhận tín hiệu! Bắt đầu xử lý truy vấn người dùng...")

async def main():
    start = time.time()
    
    # Khởi động 3 worker trước
    workers = [asyncio.create_task(worker(i)) for i in range(1, 4)]
    
    # Khởi động tác vụ nạp DB
    init_task = asyncio.create_task(database_initializer())
    
    await asyncio.gather(init_task, *workers)
    print(f"⏱️ Tổng thời gian chạy: {time.time() - start:.2f}s")

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

💡 Quan sát cùng bé:

Cả 3 workers cùng thức dậy tại đúng tích tắc thứ 3.0, ngay khi db_ready_event.set() được kích hoạt, không lệch một mili-giây nào!

PHẦN 2: asyncio.Condition – ĐIỀU HƯỚNG THEO ĐIỀU KIỆN PHỨC TẠP

1. Tại sao cần asyncio.Condition khi đã có Event?

  • Event chỉ là một chiếc công tắc nhị phân bật/tắt đơn giản: khi bật cờ, tất cả mọi task đang chờ đều thức dậy.

  • Nhưng trong thực tế, bài toán phức tạp hơn:

    • Ví dụ: Trong kho hàng, có 5 xe tải đang chờ lấy hàng. Nhà máy sản xuất thêm được 1 kiện hàng, bạn chỉ muốn đánh thức đúng 1 xe tải dậy để bốc hàng đi (nếu đánh thức cả 5 xe thì 4 xe kia mất công thức dậy rồi lại tranh nhau thất vọng).

    • asyncio.Condition chính là sự kết hợp giữa asyncio.Lock (khóa chống tranh chấp tài nguyên) và cơ chế phát thông báo có chọn lọc:

      • notify(n=1): Chỉ đánh thức đúng $n$ tác vụ đang chờ.

      • notify_all(): Đánh thức tất cả (giống Event.set()).

2. Cú pháp bắt buộc của Condition

Vì bên trong Condition có tích hợp sẵn một chiếc khóa (Lock), nên mọi thao tác đều phải nằm trong ngữ cảnh async with condition:

cond = asyncio.Condition()

# Phía chờ điều kiện:
async with cond:
    # Chờ cho đến khi biểu thức lambda trả về True
    await cond.wait_for(lambda: kho_hang > 0)
    kho_hang -= 1

# Phía cung cấp dữ liệu:
async with cond:
    kho_hang += 1
    cond.notify(1) # Chỉ đánh thức 1 tác vụ dậy lấy

3. Thực hành 2: Bài toán Kho hàng (Producer - Consumer) với Condition

Tạo file buoi4_condition_inventory.py:

import asyncio
import random

class Warehouse:
    def __init__(self):
        self.stock = 0
        self.cond = asyncio.Condition()

    async def produce(self, item_name: str):
        """Nhà sản xuất: Làm xong 1 món và thông báo cho 1 người mua"""
        await asyncio.sleep(random.uniform(1.0, 2.0))
        async with self.cond:
            self.stock += 1
            print(f"🏭 [Sản xuất] Đã thêm '{item_name}' vào kho (Hiện có: {self.stock} món)")
            # Đánh thức DUY NHẤT 1 người mua đang xếp hàng
            self.cond.notify(n=1)

    async def buy(self, customer_id: int):
        """Khách hàng: Xếp hàng chờ cho đến khi có hàng trong kho"""
        async with self.cond:
            print(f"  🛒 Khách-{customer_id}: Đang chờ hàng...")
            # Chờ cho đến khi self.stock > 0
            await self.cond.wait_for(lambda: self.stock > 0)
            
            # Khi được đánh thức, chắc chắn self.stock >= 1 và ta đang giữ Lock an toàn
            self.stock -= 1
            print(f"  🎉 Khách-{customer_id}: Mua hàng thành công! (Kho còn lại: {self.stock} món)")

async def main():
    wh = Warehouse()
    print("--- MÔ PHỎNG KHO HÀNG VỚI ASYNCIO.CONDITION ---")
    
    # 4 khách hàng đến xếp hàng trước khi hàng kịp sản xuất
    buyers = [asyncio.create_task(wh.buy(i)) for i in range(1, 5)]
    
    # Đưa hàng vào lần lượt từng món
    producers = [
        asyncio.create_task(wh.produce(f"iPhone-Batch-{i}")) 
        for i in range(1, 5)
    ]

    await asyncio.gather(*buyers, *producers)
    print("\n✅ Toàn bộ khách hàng đã mua được sản phẩm an toàn, không tranh chấp!")

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

PHẦN 3: SO SÁNH TỔNG KẾT NHANH ĐỂ GHI NHỚ

Tiêu chí asyncio.Event asyncio.Condition
Bản chất Cờ tín hiệu trạng thái bật/tắt (True/False) Bộ điều phối kèm Khóa (Lock) dựa trên trạng thái biến
Cơ chế đánh thức Đánh thức tất cả các task đang chờ (event.set()) Đánh thức có số lượng chỉ định (notify(n)) hoặc tất cả (notify_all())
Cần dùng với async with? Không cần Bắt buộc (async with condition)
Trường hợp điển hình Cổng khởi động hệ thống, cờ báo mất/có mạng Quản lý kho hàng, hồ kết nối (Pool), giới hạn tài nguyên chia sẻ

PHẦN 4: THỬ THÁCH SUY LUẬN CHO BÉ

  1. Thử thách 1 (Nút tạm dừng Game / Ứng dụng):

    • Trong một trò chơi, người chơi nhấn nút Space để PAUSE game, nhấn lần nữa để RESUME.

    • Gợi ý cho bé: Hãy dùng asyncio.Event để điều khiển vòng lặp chạy điểm số của game: khi cờ hạ xuống (clear()) thì điểm dừng tăng; khi dựng cờ (set()) thì điểm tiếp tục tăng vèo vèo.

  2. Thử thách 2 (Lỗ hổng logic):

    • Nếu dùng asyncio.Event cho bài toán kho hàng (4 khách chờ, nhà máy thêm 1 sản phẩm rồi gọi event.set()), điều gì sẽ xảy ra? Cả 4 khách có cùng lao vào trừ biến stock và làm kho bị âm hàng không?