
asyncio trong Python là gì và vì sao nó nhanh hơn threading là câu hỏi mà bất kỳ lập trình viên nào làm backend cũng gặp phải. Thư viện asyncio cho phép một luồng thực thi duy nhất xử lý hàng nghìn kết nối mạng chờ đồng thời, nhờ cơ chế chuyển quyền điều khiển khi gặp tác vụ chậm thay vì đứng chờ. Bài viết này bóc tách event loop, coroutine, task, và những lỗi phổ biến nhất khiến asyncio chạy chậm hơn cả code đồng bộ.
Bài toán: blocking I/O là gì
Hãy hình dung một web server xử lý 1000 request đồng thời. Mỗi request cần gọi ra API thanh toán bên ngoài, giả sử mỗi lần gọi mất 200 mili giây do độ trễ mạng.
Với code đồng bộ dùng threading, bạn cần 1000 thread. Mỗi thread có stack riêng (mặc định vài MB), và GIL buộc chỉ một thread chạy bytecode Python tại một thời điểm. Kết quả là bạn tốn hàng GB RAM để chờ mạng, trong khi CPU phần lớn thời gian nằm không.
Điểm mấu chốt: CPU không phải tài nguyên khan hiếm, mà thời gian chờ mới là thứ lãng phí. Một thread đang chờ socket.recv() không tốn CPU lệnh nào, nhưng vẫn khóa một slot trong thread pool.

Event loop: trái tim của asyncio
asyncio giải quyết vấn đề trên bằng cách đảo ngược thứ tự ưu tiên. Thay vì “chờ việc A xong rồi làm việc B”, nó làm “làm việc A đến chỗ phải chờ, nhảy sang việc B, quay lại A khi việc A xong”.
Trò so sánh từ cooperative multitasking giúp hình dung rõ: coroutine tự nguyện nhường quyền, không ai cưỡng ép nó.
Event loop chạy trong một thread của hệ điều hành. Nó dùng bộ ghép I/O đa đường của hệ điều hành để theo dõi hàng nghìn file descriptor cùng lúc mà không cần thread cho mỗi cái:
| Hệ điều hành | Cơ chế đa đường I/O | Ghi chú |
|---|---|---|
| Linux | epoll |
Mở rộng tuyến tính, chi phí thấp |
| macOS, BSD | kqueue |
Cùng triết lý, API khác chút |
| Windows | IOCP |
Cơ chế completion-based |
Chi tiết về epoll được mô tả tại trang Wikipedia về epoll. Cơ chế nền tảng được trình bày chi tiết trong tài liệu asyncio event loop.

Coroutine, Task, awaitable: phân biệt thế nào
Đây là ba khái niệm hay bị lẫn, và hiểu đúng thì phần còn lại rất đơn giản.
Coroutine
Gọi một hàm async def không chạy code bên trong, nó chỉ tạo ra một coroutine object. Bạn phải await nó hoặc giao cho Task thì nó mới thực sự chạy. Đây là lý do quen thuộc cảnh báo coroutine was never awaited xuất hiện khi bạn quên await.
Task
Task là lớp bọc coroutine, lên lịch chạy trên event loop, và quản lý vòng đời của nó với cancel(), done(), result(), exception(). Tạo task bằng asyncio.create_task(coro), hàm được khuyến nghị hơn ensure_future() vì tường minh hơn.
awaitable
Khái niệm bao trùm: bất cứ thứ gì await được, gồm coroutine, Task, và cả Future. Tài liệu chính thức mô tả đầy đủ ở asyncio Task documentation.
async def, await, asyncio.run
Hình dạng tối thiểu của một chương trình asyncio luôn giống nhau: một entry point gọi asyncio.run(), bên trong có một hoặc nhiều coroutine được await.
asyncio.run(coro) tạo event loop mới, chạy coroutine tới khi hoàn thành, rồi đóng loop lại. Đây là entry point được khuyến nghị từ Python 3.7 vì nó loại bỏ mọi mớ hỗn trợ của get_event_loop() thời xưa.
Từ Python 3.11 có thêm asyncio.Runner cho trường hợp cần kiểm soát loop factory và context variables, thay cho asyncio.run() trong các chương trình phức tạp.
Chạy song song nhiều tác vụ
Điểm mạnh lớn nhất của asyncio là chạy nhiều tác vụ I/O đồng thời với gather():
asyncio.gather(*aws, return_exceptions=False) chạy tất cả awaitable được truyền vào, và trả về kết quả theo đúng thứ tự đầu vào, không phải theo thứ tự hoàn thành.
Có một chi tiết dễ gây hiểu nhầm: mặc định return_exceptions=False nghĩa là chỉ cần một tác vụ ném exception, gather sẽ ném ngay exception đó ra ngoài mà không chờ các tác vụ còn lại xong. Các tác vụ còn lại vẫn chạy tiếp trong nền. Nếu muốn thu thập mọi lỗi, đặt return_exceptions=True.
Lỗi phổ biến nhất: blocking call lọt vào async def
Đây là nguyên nhân số một khiến code asyncio chậm hơn cả threading. Bất kỳ hàm đồng bộ nào chạy bên trong coroutine đều chặn toàn bộ event loop:
Hai dòng time.sleep() và requests.get() trong một coroutine sẽ đứng yên toàn bộ server cho tới khi chúng trả về. Mọi request khác đang chờ phải xếp hàng. Sửa bằng cách dùng asyncio.sleep() và thư viện bất đồng bộ.
Nguyên tắc nhớ: nếu tên hàm bạn gọi trong async def không phải awaitable, hãy nghi ngờ ngay.

Quản lý thời gian chờ và hủy tác vụ
Đặt timeout cho tác vụ bất đồng bộ nên dùng API riêng thay vì tự quản lý:
asyncio.wait_for(aw, timeout)chờ tác vụ trong thời gian giới hạn. Hết thời gian thì tác vụ bị hủy vàTimeoutErrorđược ném ra.asyncio.shield(aw)bảo vệ tác vụ khỏi bị hủy từ bên ngoài. Nếu chỉ muốn cho phép hủy, nhưng vẫn muốn tác vụ chạy nốt phần dọn dẹp tài nguyên, đây là công cụ đúng.
Biến theo ngữ cảnh thay cho biến luồng
threading.local() ràng buộc dữ liệu theo luồng, vô dụng khi bạn chuyển sang mô hình một luồng nhiều tác vụ. contextvars.ContextVar giải quyết đúng vấn đề đó: dữ liệu gắn với ngữ cảnh tác vụ và truyền đi xuyên suốt các điểm await.
Đây là nền tảng cho request-scoped context trong framework web hiện đại. Tài liệu chính thức: contextvars documentation.
asyncio và GIL: chúng liên gì
GIL vẫn tồn tại và asyncio không xóa nó. Nhưng nó không chặn được lợi thế của asyncio, vì lý do chính GIL tồn tại là để bảo vệ cấu trúc dữ liệu khi nhiều luồng chạy bytecode đồng thời.
Với asyncio, chỉ một luồng chạy bytecode Python tại mọi thời điểm nên GIL không bao giờ tranh chấp. Và GIL được nhả ra đúng lúc cần, khi luồng chờ I/O qua bộ chọn. Kết quả: hàng nghìn tác vụ I/O chạy song song thực sự trong một luồng.
Giới hạn rõ ràng: asyncio không giúp ích cho tác vụ CPU-bound. Vẫn cần multiprocessing, hoặc ProcessPoolExecutor, và đó là việc khác hẳn.
asyncio trong framework web
FastAPI là ví dụ rõ nhất trong hệ sinh thái Python. Hàm xử lý route khai báo bằng async def được chạy trực tiếp trên event loop của ASGI server như Uvicorn. Framework có hướng dẫn riêng về lập trình bất đồng bộ, giải thích rõ khi nào nên dùng async def và khi nào nên dùng def thường để đẩy việc nặng xuống thread pool.
Một điểm hay gây nhầm: khai báo async def cho hàm xử lý mà bên trong lại gọi một hàm blocking sẽ làm hỏng toàn bộ hiệu năng của server, chứ không phải của riêng request đó.
uvloop: tăng tốc cho tác vụ mạng nặng
uvloop là bản thay thế event loop của asyncio, viết trên libuv, cài đặt được như một event loop policy thay thế. Với workload nhiều kết nối mạng, cải thiện hiệu năng đáng kể nhờ bộ lập lịch và cơ chế đa đường I/O nhanh hơn bản chuẩn.
Lưu ý thực tế: kiểm tra hỗ trợ nền tảng trước khi dùng, và luôn có sẵn đường lùi về event loop chuẩn.
Khi nào nên chọn asyncio, khi nào nên không
| Tình huống | Có dùng asyncio | Lý do |
|---|---|---|
| API gateway gọi hàng trăm dịch vụ ngoài | Có | Đây là I/O-bound thuần |
| WebSocket với nhiều kết nối đồng thời | Có | Hàng nghìn kết nối trong một tiến trình |
| Xử lý video, mã hóa, training model | Không | CPU-bound, cần multiprocessing |
| Script nhỏ, tuần tự, vài request | Không | Overhead của abstraction lớn hơn lợi ích |
| Thư viện bạn cần chỉ có bản đồng bộ | Cần kiểm tra | Blocking call lọt vào sẽ phá hiệu năng |
Checklist trước khi kết luận code chậm
- Quét tìm blocking call trong
async def:requests,time.sleep, đọc file đồng bộ. - Kiểm tra có
awaitcoroutine nào không hay không. - Đo phân bố thời gian chờ I/O so với CPU, đừng đoán.
- Xem thêm tài liệu asyncio chính thức cho phần debug và task introspection.
Kết luận
asyncio thắng ở một việc cụ thể: quản lý rất nhiều tác vụ đang chờ I/O, với chi phí bộ nhớ thấp hơn hẳn thread. Nó thua ở mọi việc CPU-bound, và dễ bị phá hiệu năng nếu bạn lỡ tay đặt một blocking call vào giữa.
Hiểu event loop, phân biệt rõ coroutine với Task, và kiểm tra blocking call là ba điều đủ để dùng asyncio hiệu quả trong phần lớn dự án backend.
