Python Async, vì sao vẫn chưa trở thành xu hướng chủ đạo?
(tonybaloney.github.io)Python Async, vì sao vẫn chưa trở thành xu hướng chủ đạo?
asyncio của Python là một công cụ mạnh mẽ có thể giảm thời gian chờ và tăng đáng kể hiệu quả của chương trình trong các môi trường có nhiều tác vụ I/O (vào/ra). Tuy nhiên, dù có nhiều ưu điểm, không phải mọi lập trình viên Python đều tích cực sử dụng nó, và có một vài lý do mang tính nền tảng đằng sau điều này.
1. Đầu óc trở nên rối hơn: gánh nặng nhận thức
Rào cản lớn nhất là độ phức tạp. Mã đồng bộ có tính trực quan vì ta chỉ cần lần theo luồng thực thi tuần tự từ trên xuống dưới như khi đọc một cuốn sách.
Nhưng mã bất đồng bộ thì khác. Nó giống như một đầu bếp, trong lúc luộc mì để làm pasta (thời gian chờ), đồng thời tranh thủ làm nước sốt, và khi còn thời gian thì sơ chế rau củ. Kết quả là món ăn có thể hoàn thành nhanh hơn, nhưng trong đầu người đầu bếp luôn phải để ý nhiều trạng thái công việc như “mì chín đến đâu rồi?”, “nước sốt có bị cháy không?”.
Vì luồng thực thi của mã liên tục chuyển từ chỗ này sang chỗ khác, nên rất khó nắm bắt tác vụ nào đang chạy và tác vụ nào đang chờ. Điều này khiến quá trình gỡ lỗi, đặc biệt là khi cần truy vết nguyên nhân gây bug, trở nên rất khó khăn.
2. Hệ sinh thái tách rời: khả năng tương thích thư viện
Một vấn đề khác của Python là hệ sinh thái thư viện bị chia thành “đồng bộ” và “bất đồng bộ”. Rất nhiều thư viện nổi tiếng và tiện dụng (ví dụ: thư viện HTTP chuẩn requests hay nhiều ORM) chỉ hoạt động theo cách đồng bộ.
Nếu vô tình dùng thư viện đồng bộ trong mã bất đồng bộ, thì trong thời gian đoạn mã đồng bộ đó chạy, “event loop” — lợi thế lớn nhất của bất đồng bộ — sẽ bị chặn hoàn toàn. Điều này giống như mở nhiều làn đường nhưng chỉ sử dụng đúng một làn, khiến việc dùng bất đồng bộ mất đi ý nghĩa. Để giải quyết vấn đề này, bạn phải học và áp dụng các thư viện riêng có hỗ trợ bất đồng bộ (aiohttp, asyncpg, v.v.), và điều đó càng làm đường cong học tập trở nên dốc hơn.
3. Mỗi lần chỉ xử được một việc: GIL (khóa thông dịch toàn cục)
GIL (Global Interpreter Lock) là một trong những đặc điểm cố hữu của Python, cơ chế giới hạn để trong một tiến trình, dù có nhiều luồng thì tại cùng một thời điểm cũng chỉ một luồng duy nhất được thực thi. asyncio hoạt động trên một luồng đơn nên không xung đột trực tiếp với GIL, nhưng sự tồn tại của GIL vẫn giới hạn phạm vi ứng dụng của async.
asyncio được tối ưu để tận dụng thời gian chờ I/O (chờ phản hồi mạng, chờ đọc tệp, v.v.). Nhưng nếu bên trong hàm async có một tác vụ nặng về CPU với tính toán rất phức tạp, thì toàn bộ event loop sẽ bị dừng cho đến khi phép tính đó hoàn tất. Trong khoảng thời gian đó, các tác vụ I/O khác không thể làm gì ngoài việc chờ. Cuối cùng, với những công việc thực sự cần xử lý song song, vẫn phải dùng các kỹ thuật khác như multiprocessing.
4. Hy vọng cho tương lai: Python 3.14 và việc loại bỏ GIL
Tuy nhiên, có một tín hiệu rất đáng hy vọng đối với những giới hạn này. Đó là xu hướng loại bỏ GIL theo cách tùy chọn, được giới thiệu ở mức thử nghiệm từ Python 3.13 và được kỳ vọng sẽ trưởng thành hơn trong phiên bản 3.14.
Sự thay đổi này, được thúc đẩy thông qua đề xuất PEP 703, hướng tới mục tiêu cho phép lập trình viên chạy mã Python mà không cần GIL nếu họ muốn. Nếu điều này trở thành hiện thực, Python sẽ có thể đạt được đa luồng thực sự, nơi nhiều luồng có thể đồng thời tận dụng nhiều lõi CPU.
Khi kết hợp với asyncio, điều này có thể tạo ra sức mạnh cộng hưởng rất lớn. Tác vụ I/O có thể được xử lý hiệu quả bằng asyncio, còn các tác vụ CPU nặng tính toán có thể được chuyển sang các luồng riêng để xử lý song song mà không còn bị ràng buộc bởi GIL. Sự thay đổi này được kỳ vọng sẽ trở thành bước ngoặt lớn cho hệ sinh thái Python và phá bỏ nhiều rào cản từng cản trở việc chấp nhận lập trình async.
9 bình luận
Có vẻ như GIL được nhắc đến hơi đột ngột thì phải.. ngay cả khi GIL bị loại bỏ
nếu muốn dùng multithread cho cả tác vụ I/O bound lẫn CPU bound
thì có lẽ nên chọn một phương án thay thế khác thay vì Python..
Dường như
asynciokhá bị những người đào sâu Python không thích.Tôi cũng thường nghe ý kiến rằng lẽ ra
geventmới nên trở thành dòng chính.Tôi đồng ý rằng không thể kỳ vọng định hướng hiện tại đối với GIL sẽ trở thành một lựa chọn không hề kém cạnh ngay cả khi so với "các phương án thay thế khác"
nhưng tôi nghĩ lập luận rằng nên chọn một phương án khác ngoài Python không nên dẫn đến giọng điệu cho rằng không có vấn đề gì, mà phải dẫn đến giọng điệu thừa nhận rằng có vấn đề chứ?
Mọi người dùng
asynciokhá nhiều đấy.. dùng ổn.. có một giới hạn là việc hủy tác vụ được thiết kế theo kiểu edge-triggered (không phải level-triggered), nhưng thực ra cũng không mấy khi phải viết code vừa nhận biết việc hủy tác vụ vừa xử lý graceful cho lắm, và vấn đề lớn hơn là event loop chỉ giữ weak reference tới task nên nó có thể biến mất do GC.. nhưng chuyện đó được giải quyết bằng structured concurrency.Với hầu hết các tác vụ I/O chính thì không có vấn đề gì khi tìm thư viện hỗ trợ
asyncio..Còn GIL? Thật ra không liên quan nhiều đâu.. ngay từ cách tiếp cận dùng
asynciođể chạy song song các tác vụ CPU intensive đã hơi kỳ rồi.. nếu GIL được cải thiện thì nó sẽ hữu ích cho multithreading CPU intensive.. còn async là để vận hành hiệu quả nhất có thể ở những đoạn nghẽn I/O...Dù sao thì kết luận là.. tuy có một số vấn đề trong thiết kế, nhưng để đạt được mục tiêu thì tôi vẫn dùng tốt trong production mà không gặp vấn đề gì đáng kể.
Bạn đã từng gặp trường hợp task bị
gcthu gom chưa?Tất nhiên, ngay cả tôi cũng đã dùng
asynciođến mức phát ngán trong production, nhưng tôi vẫn chưa thấy trải nghiệm sử dụng hiện tại đủ thỏa mãn để có thể đánh giá là “mình đang dùng nó rất tốt.”asynciohiện tại được thiết kế với GIL là tiền đề, nói theo một cách nào đó thì nó là chiến lược né tránh GIL, nên GIL không trực tiếp tương tác vớiasyncio.Nhưng nếu nhìn từ góc độ toàn bộ lập trình đồng thời vận hành dựa trên
asyncio, thì tôi nghĩ cách nói rằng GIL không liên quan sẽ thành ra giống như kiểu “vì là Python nên không làm được cũng là chuyện đương nhiên.”Tôi cứ dùng joblib thôi
Vấn đề của Asyncio không nằm ở độ khó của lập trình bất đồng bộ vốn đã khó, mà là ở chất lượng kém. Một thiết kế vứt bỏ tính nhất quán và tính phổ quát thì trong Python cũng chẳng phải chuyện hiếm, nhưng những thứ như ProactorEventLoop thì đến cả lỗi gây gián đoạn dịch vụ đã được báo cáo từ 5 năm trước vẫn còn chưa được xử lý.
Với những người ở thế buộc phải dùng nó, kiểu bài viết như thế này thật sự rất khó để chỉ cười cho qua.
Tất nhiên, lý do lớn hơn có thể là vì do GIL nên lợi ích có thể thu được ngay từ đầu vốn đã ít hơn so với các môi trường khác.
Tôi cho rằng cách nói rằng nếu không có GIL thì có thể tạo ra hiệp lực là gần như mang tính ngụy biện. Với một vận động viên chạy bộ bị mất một chân, nếu gắn cho họ một chân giả dù còn nhiều bất tiện, thì đó có gọi là "hiệp lực" không?