Ruby Async: Tại Sao Nó Là Vũ Khí Bí Mật Cho AI và Tốt Hơn Python?
Lê Lân
1
Cuộc Cách Mạng Async Trong Ruby: Lý Do Tại Sao Async Mới Là Tương Lai Của Lập Trình Đồng Thời Với Ứng Dụng LLM
Mở Đầu
Trở về Ruby sau một thập kỷ làm việc trong hệ sinh thái async của Python giống như một chuyến du hành ngược thời gian, khiến tôi thắc mắc: "Async revolution đâu rồi? Tại sao mọi người vẫn trung thành với thread?"
Sau nhiều năm làm việc với Python và chứng kiến cú nhảy vọt của asyncio, tôi nhận thấy Ruby vẫn giữ mô hình concurrency thread-based truyền thống với Sidekiq, GoodJob, SolidQueue. Trong khi Python hầu như mọi thư viện đều có bản async, Ruby dường như chưa bắt kịp cuộc cách mạng đồng thời này.
Tuy nhiên, trong quá trình xây dựng RubyLLM và Chat with Work, tôi nhận ra rằng giao tiếp với các mô hình ngôn ngữ lớn (LLM) chính là ứng dụng "killer app" của async trong Ruby. Các yêu cầu đặc biệt về streaming, kết nối lâu dài và hàng nghìn cuộc hội thoại đồng thời đã phơi bày sức mạnh vượt trội của async. Qua đó, tôi đã khám phá ra Ruby có một cách tiếp cận async thực sự tốt hơn cả Python — không cần phải viết lại toàn bộ codebase, không cần cú pháp mới, mà hiệu suất lại vượt trội.
Trong bài viết này, tôi sẽ phân tích kỹ lưỡng vì sao async là xu hướng tất yếu, lợi ích của async trong Ruby, và làm thế nào bạn có thể chuyển đổi ứng dụng Rails sang async một cách gần như không cần thay đổi.
1. Tại Sao Giao Tiếp Với LLM Lại Là Thử Thách Cho Mô Hình Thread-Based
1.1. Kiệt Quệ Slot Worker (Slot Starvation)
Giả sử bạn có hàng đợi công việc với 25 worker thread:
classStreamAIResponseJob < ApplicationJob
defperform(chat, message)
chat.ask(message) do |chunk|
broadcast_chunk(chunk)
end
end
end
Mỗi luồng chiếm một slot trong số 25 slot này trong suốt thời gian dài (30-60 giây) đợi nhận dữ liệu từng token, dù phần lớn thời gian thread bị idle. Khi khách hàng thứ 26 gọi đến, họ phải chờ vì tất cả thread đang bận dù server chưa thực sự bận.
1.2. Phí Tài Nguyên (Resource Multiplication)
Mỗi thread đòi hỏi:
Kết nối database riêng biệt (ví dụ 25 thread = 25 kết nối)
Bộ nhớ stack và tài nguyên hệ điều hành
Chi phí tạo và quản lý luồng
Nếu có 1000 cuộc hội thoại đồng thời, bạn cần 1000 thread và 1000 kết nối DB cho những thread hầu như không làm hoạt động nào. Điều này rất lãng phí.
1.3. Chi Phí Về Hiệu Năng (Performance Overhead)
Các benchmark thực tế chỉ rõ:
Hạng mục
Thời gian (ước tính)
Tạo một thread
~80 micro giây (μs)
Context switch thread
~1.3 micro giây (μs)
Throughput tối đa
~5,000 request/giây
Khi xử lý hàng nghìn request streaming, những chi phí nhỏ này cộng lại tạo ra độ trễ đáng kể.
1.4. Thách Thức Phân Tán (Scalability)
Tạo ra 10.000 thread khiến hệ điều hành khó khăn trong việc điều phối, gây quá tải tài nguyên. Các ứng dụng AI hiện đại cần quản lý quy mô lớn, điều này không thể thực hiện hiệu quả với thread.
Tóm lại: Tất cả các vấn đề trên đều phát sinh do mô hình concurrency dựa trên thread không phù hợp với đặc thù giao tiếp streaming và LLM.
2. Hiểu Về Đồng Thời (Concurrency): Threads vs Async
2.1. Quá Trình Tổ Chức: Process, Thread, Fiber
Ta có thể so sánh dễ hiểu:
Process là như một phòng ban độc lập, không chia sẻ bộ nhớ.
Thread là các nhân viên trong cùng một phòng ban, chia sẻ tài nguyên nhưng cần phối hợp tránh trùng lặp.
Fiber là các công việc con mà một người có thể luân phiên làm, tự nguyện nhường chỗ cho nhau.
2.2. Lập Lịch: Điểm Cốt Lõi
Sự khác biệt quan trọng nhất giữa thread và fiber là cách chuyển đổi tác vụ được điều khiển.
Thread (Preemptive Multitasking): Hệ điều hành tự động can thiệp, ngắt luồng khi cần.
threads = 10.times.map do |i|
Thread.new do
expensive_calculation(i)
fetch_from_api(i)
process_result(i)
end
end
Thread bị gián đoạn bất cứ lúc nào, cần OS quản lý, tiêu tốn tài nguyên hệ thống.
Fiber (Cooperative Concurrency): Fiber tự nguyện yield khi chờ I/O.
Asyncdo
fibers = 10.times.map do |i|
Asyncdo
expensive_calculation(i)
fetch_from_api(i)
process_result(i)
end
end
end
Fiber không bị can thiệp khi đang chạy CPU-bound, chỉ yield ở điểm chờ I/O, quản lý hoàn toàn ở người dùng.
2.3. GVL Của Ruby Và Lợi Thế Của Fiber
Ruby có Global VM Lock (GVL) cho phép chỉ một thread chạy Ruby code tại một thời điểm.
Với CPU-bound tasks, thread không tăng tốc đáng kể do GVL.
Với I/O-bound tasks, thread có thể song song khi GVL được thả ra.
Tuy nhiên, nếu chỉ giúp I/O mà phải chịu overhead lớn của thread thì có vẻ không hợp lý.
Fiber phù hợp với mô hình GVL của Ruby hơn do:
Lập lịch hợp tác tự nhiên
Giảm overhead hệ điều hành
3. Lợi Thế Của Async Với Fiber Trong Ruby
3.1. Mô Hình I/O Multiplexing
Thay vì một thread cho một kết nối, fiber cho phép một thread duy nhất giám sát hàng nghìn I/O nhờ epoll/kqueue/io_uring.
Asyncdo
task1 = Async { socket1.read }
task2 = Async { socket2.read }
task3 = Async { socket3.read }
end
Kernel giám sát nhiều socket trên một system call duy nhất, fiber được thức dậy khi dữ liệu tới — rất tiết kiệm tài nguyên.
3.2. Hiệu Năng So Sánh Fiber Và Thread (Ruby 3.4)
Tiêu chí
Fiber
Thread
Tỷ lệ
Thời gian tạo (μs)
~3
~80
Fiber nhanh hơn 20x
Chuyển đổi context (μs)
~0.1
~1.3
Fiber nhanh hơn 10x
Throughput (requests/s)
~80,000
~5,000
Fiber cao hơn 15x
3.3. Quy Mô Và Quản Lý Tài Nguyên
Fiber quản lý trong user-space, giảm phụ thuộc tài nguyên OS.
Có thể tạo hàng chục nghìn fiber đồng thời mà không ảnh hưởng tới hệ điều hành.
Resource như kết nối DB có thể được chia sẻ.
Fiber không chỉ nhanh hơn mà còn mở rộng quy mô mà thread không thể đạt được.
4. Async Ruby Giải Quyết Toàn Bộ Các Thách Thức Của Ứng Dụng LLM
Thách thức
Giải pháp Async Fiber
Slot Starvation
Tạo fiber theo nhu cầu, không cố định số worker
Resource Multiplication
Dùng ít kết nối DB hơn, chia sẻ tài nguyên
Performance Overhead
Tạo và switch fiber nhanh hơn
Scalability Challenge
Hỗ trợ 10,000+ fiber đồng thời mà không ảnh hưởng OS
Async Ruby hoàn toàn phù hợp với tính chất đặc thù của giao tiếp LLM: streaming token-by-token, kết nối dài, concurrency cao.
5. Hệ Sinh Thái Async Ruby: Không Cần Viết Lại Mã Nguồn
5.1. Async Gem – Nền Tảng Vững Chắc
require'async'
require'net/http'
Asyncdo
responses = 1000.times.map do
Asyncdo
uri = URI("https://api.openai.com/v1/chat/completions")
Không cần sửa đổi code job hay channels. Chỉ cần deploy và tận hưởng hiệu suất vượt trội.
7. Khi Nào Nên Dùng Thread, Khi Nào Dùng Async?
Threads:
Công việc CPU-intensive
Yêu cầu cô lập thực sự
Thư viện native không hỗ trợ fiber-safe
Async:
Công việc I/O-bound
Các cuộc gọi API, WebSocket, streaming
Ứng dụng LLM và AI
Kết Luận
Ruby đã chọn một hướng đi thông minh: phát triển async mà không phá vỡ cách viết code truyền thống.
Không cần viết lại stack
Không cần đổi syntax
Hiệu suất vượt trội mà vẫn giữ nguyên codebase.
Giao tiếp LLM chính là trường hợp thực tế giúp ta thấy rõ sức mạnh của async Ruby. Cộng đồng do Samuel Williams dẫn dắt đã tạo ra một hệ sinh thái async mạnh mẽ mà mọi Rubyist nên tận dụng.
Nếu bạn phát triển ứng dụng AI, streaming, hay hệ thống cần concurrency cao, async Ruby không chỉ là lựa chọn — đó là lợi thế cạnh tranh. Đừng ngần ngại thử nghiệm và chuyển đổi ngay hôm nay!