Xin chào các bạn, trong bài viết trước chúng ta đã cùng thảo luận về các phương pháp để thực hiện concurrency programming (lập trình đồng thời) trong Python. Concurrency programming không chỉ giúp chúng ta by pass sự ảnh hưởng của GIL (Global Interpreter Lock) mà còn giúp cải thiện hiệu suất của ứng dụng.
Tiếp nối serie Python Guru ngày hôm nay, chúng ta sẽ cùng tìm hiểu về cách thức lập trình bất đồng bộ trong Python.
1. Đôi nét về lập trình đồng bộ và bất đồng bộ
Trong lập trình đồng bộ (synchronous programming) truyền thống, mỗi tác vụ sẽ được thực hiện tuần tự. Tức là tác vụ này phải hoàn thành trước khi tác vụ tiếp theo bắt đầu.
Lập trình bất đồng bộ (asynchronous programming) trong Python là một kỹ thuật cho phép bạn viết mã có thể xử lý nhiều tác vụ đồng thời mà không chặn luồng chính (main thread) của chương trình. Điều này đặc biệt hữu ích trong các tác vụ I/O-bound như call API requests, truy vấn cơ sở dữ liệu, đọc/ghi tệp, etc, nơi mà việc chờ đợi các tác vụ này hoàn thành có thể khiến hiệu suất ứng dụng bị giảm sút.
2. Lập trình bất đồng bộ trong Python
2.1 Coroutine
Từ Python 3.4+ với sự ra đời của Asyncio, thư viện chuẩn của Python dành cho lập trình bất đồng bộ đã khiến cho việc lập trình các ứng dụng bất đồng bộ trên python trở nên dễ dàng hơn bao giờ hết.
Coroutine là một khái niệm quan trọng khi nhắc đến lập trình bất đồng bộ trong Python. Coroutine hiểu một cách đơn giản các hàm có thể tạm dừng và tiếp tục thực thi tại các điểm khác nhau. Nó cho phép chương trình thực thi các đoạn mã một cách không đồng bộ bằng cách tạm dừng thực thi tác vụ tại các điểm chờ (await) và tiếp tục lại khi công việc đó hoàn thành.
Python sử dụng cú pháp async def để định nghĩa một hàm là hàm bất đồng bộ (một coroutine) và từ khóa await để tạm dừng và chờ kết quả từ các task vụ (python coroutines).
Await được sử dụng để chờ một coroutine hoàn thành mà không chặn luồng chính. Khi gặp await, event loop sẽ tạm dừng coroutine hiện tại và chuyển sang thực hiện coroutine khác.(Cú pháp và ý tưởng khá giống với NodeJS).
2.2 Eventloop
Eventloop chính là bộ não đứng đằng sau điều phối hoạt động của toàn bộ các coroutines trong chương trình Python.
Các hoạt động chính của Event Loop
- Scheduling: Event Loop lên lịch cho các coroutine và các tác vụ không đồng bộ khác để thực thi.
- Executing: Event Loop thực thi các coroutine đến các điểm chờ (await) của chúng, sau đó tạm dừng chúng và chuyển sang các tác vụ khác.
- Handling I/O: Event Loop có thể chờ cho các hoạt động I/O (như đọc/ghi tệp hoặc kết nối mạng) hoàn thành mà không chặn luồng chính.
Bạn hãy tưởng tượng Eventloop giống như một người bếp trưởng tài năng có khả năng điều phối hoạt động của toàn bộ nhà hàng đảm bảo rằng tất cả các thực khách của mình đều sẽ nhận được món ăn mà thực khách order.
Sau khi nhận được các order từ các bạn phục vụ, bếp trưởng sẽ nhận danh sách các order và tiến hành điều phối gian bếp gồm các bếp phó, thợ phụ (workers) thực hiện món ăn mà thực khách yêu cầu.
Khi một món ăn được hoàn thành, bếp trưởng sẽ ra dấu cho các nhân viên phục vụ để họ giao món ăn tới chính xác bàn mà thực khách yêu cầu để khách hàng có thể thưởng thức trong thời gian chờ đợi các món ăn khác được bày lên. Cứ như thế lần lượt từng khách, từng bàn đều được phục vụ cho đến khi tất cả các bàn đều được hoàn thành. Mọi người đều happy, có món ăn để thưởng thức trong lúc chờ đợi.
2.3 FastAPI
Ứng dụng của Couroutines và Eventloop được FastAPI ứng dụng để tạo ra một Framework cho phép triển các ứng dụng Python web hiệu năng cao.
Khi bạn khởi chạy một ứng dụng FastAPI (ví dụ, sử dụng Uvicorn hoặc Gunicorn với uvicorn workers), một event loop sẽ được tạo ra và tất cả các yêu cầu đến được xử lý trong event loop này.
Bởi vì mọi yêu cầu đều chạy trên cùng một event loop, FastAPI có thể xử lý hàng ngàn yêu cầu đồng thời một cách hiệu quả mà không cần tạo ra nhiều thread hay process. Điều này giúp tận dụng tối đa khả năng của lập trình bất đồng bộ, đặc biệt là trong các tác vụ I/O-bound như kết nối cơ sở dữ liệu, gọi API bên ngoài, hoặc xử lý tệp.
3. Một số lỗi phổ biến
Khi làm việc với framework như FastAPI hay khi làm việc với asyncio, có một số lỗi phổ biến mà bạn cần tránh để đảm bảo mã nguồn của bạn hoạt động đúng cách và hiệu quả.
3.1 Không sử dụng await trong async function
Lỗi: Khi khai báo một hàm async, nhưng không sử dụng await để chờ kết quả của một coroutine khác. Đây là một lỗi phổ biến mà nhiều lập trình viên mắc phải khi không thực hiện lấy kết quả của một hàm async bằng từ khóa await.
Kết quả: Hàm sẽ không thực hiện bất đồng bộ và có thể gây ra lỗi hoặc hoạt động không như mong đợi.
Giải pháp: Đảm bảo rằng bạn sử dụng await khi gọi một coroutine từ một hàm async.
3.2 Chặn event loop bằng các tác vụ đồng bộ
Lỗi: Thực hiện các tác vụ blocking (đồng bộ) trong một hàm async, như gọi các hàm I/O đồng bộ hoặc chạy các tác vụ tốn nhiều CPU. Đây là cũng một lỗi hay gặp, khi call một hàm sync trong các hàm bất đồng bộ làm thay đổi đặc tính của việc sử dụng event loop.
Kết quả: Event loop bị chặn, làm giảm hiệu suất và lợi ích của lập trình bất đồng bộ.
Giải pháp: Khi chúng ta install bất kỳ thư viện nào hãy đảm bảo rằng các thư viện đó hỗ trợ các phiên bản bất đồng bộ với các hàm đó. Bên cạnh đó ta chạy các tác vụ blocking trong một thread hoặc process riêng.
3.3 Quên không close event loop
Lỗi: Không đóng event loop sau khi đã hoàn thành các tác vụ.
Kết quả: Event loop có thể không được giải phóng đúng cách, gây ra rò rỉ tài nguyên.
Giải pháp: Sử dụng asyncio.run() ( để tự động đóng event loop) hoặc đảm bảo đóng loop thủ công bằng cách gọi loop.close().
4. Tổng kết
Ngày hôm nay chúng ta đã cùng thảo luận cách thức lập trình bất đồng bộ trong Python. Về cách sử dụng Asyncio, Coroutine, Eventloop và một số lỗi hay gặp trong quá trình lập trình bất đồng bộ trong Python.
Cảm ơn các bạn đã đọc bài viết.
Again mình là Phan, một chàng developer tò mò và tận tâm.
Happy coding!