Event loop trong Python

Event loop là một vòng lặp (vô hạn hoặc hữu hạn) chịu trách nhiệm duyệt các task, lập lịch thực thi và duy trì hàng đợi các task đang chờ chạy. Khi một task được tạo ra, event loop đưa task đó vào queue để quyết định thời điểm thực thi tiếp theo. Đây là trái tim của lập trình bất đồng bộ trong các ngôn ngữ như Python hay JavaScript.

Cơ chế hoạt động của event loop

Trong mô hình coroutine, mỗi task chỉ chạy một đoạn code nằm giữa hai lần nhường quyền điều khiển (giữa hai lần yield). Sau mỗi lần chạy:

Nhờ cơ chế đó, event loop luân phiên nhiều task dùng chung bộ nhớ mà không cần tạo thread hoặc process riêng cho từng tác vụ. Event loop chỉ dừng khi queue rỗng hoặc bị terminate theo policy mà người lập trình lựa chọn.

flowchart LR
    New["Task được tạo"] --> Q[("Queue")]
    Q --> Get["Lấy task ra"]
    Get --> Exec["execute: chạy tới yield"]
    Exec --> Done{"Task xong?"}
    Done -->|chưa| Q
    Done -->|rồi| Wait["Lập lịch cho waiter"]
    Wait --> Q

Mô hình Task tối giản

Một Task có thể được biểu diễn như một wrapper quanh coroutine:

class Task:
    def __init__(self, waiter=None):
        self._task = iter(self)
        self._waiter = waiter

    def __iter__(self):
        yield self

    def execute(self):
        try:
            ret = next(self._task)
        except StopIteration:
            return self._waiter
        else:
            return ret

Cấu trúc này biến việc điều phối coroutine thành một giao thức đơn giản giữa task và event loop: chạy một bước, nhận kết quả, rồi quyết định lập lịch cho bước tiếp theo.

Mô hình EventLoop tối giản

Một EventLoop tối giản chỉ cần hai thành phần: một queue chứa các task chờ chạy, và một vòng lặp lấy task ra, gọi execute(), rồi đưa task tiếp theo vào queue nếu cần.

class EventLoop:
    def __init__(self):
        self._queue = queue.Queue()

    def push_task(self, *tasks):
        for task in tasks:
            self._queue.put(task)

    def _run_by(self, stop_fn):
        while not stop_fn():
            task = self._queue.get()
            next_task = task.execute()
            if next_task:
                self.push_task(next_task)

    def run_until_complete(self):
        self._run_by(self._queue.empty)

    def run_forever(self):
        self._run_by(lambda: False)

Mô hình này cho thấy bản chất của event loop không nằm ở cú pháp async/await mà nằm ở bộ lập lịch xoay vòng qua các task và quản lý trạng thái của chúng.

Lập lịch xen kẽ và tương đương với async/await

Ví dụ Ping chờ 3 giây và Pong chờ 2 giây minh họa cách hai task chạy xen kẽ: event loop liên tục đưa lại các task chưa xong vào queue, nên Pong hoàn thành trước Ping dù cả hai cùng khởi tạo từ đầu. Cách chạy này tương ứng với asyncio.gather() kết hợp await asyncio.sleep(...).

Đối chiếu hai cách viết cho thấy: yield trong mô hình tối giản đóng vai trò như await, và đối tượng waiter chính là task gọi await đang chờ kết quả. yield là điểm task nhường quyền điều khiển và cũng là điểm lưu trạng thái để tiếp tục đúng vị trí cũ - tương tự một điểm trap của task đối với event loop.