Nếu bạn phải hỏi câu hỏi này thì có lẽ bạn không quen với những gì hầu hết các ứng dụng / dịch vụ web làm. Bạn có thể nghĩ rằng tất cả các phần mềm làm điều này:
user do an action
│
v
application start processing action
└──> loop ...
└──> busy processing
end loop
└──> send result to user
Tuy nhiên, đây không phải là cách các ứng dụng web, hoặc thực sự là bất kỳ ứng dụng nào có cơ sở dữ liệu làm back-end, hoạt động. Các ứng dụng web làm điều này:
user do an action
│
v
application start processing action
└──> make database request
└──> do nothing until request completes
request complete
└──> send result to user
Trong trường hợp này, phần mềm dành phần lớn thời gian chạy bằng 0% thời gian CPU chờ cơ sở dữ liệu quay trở lại.
Ứng dụng mạng đa luồng:
Các ứng dụng mạng đa luồng xử lý khối lượng công việc trên như thế này:
request ──> spawn thread
└──> wait for database request
└──> answer request
request ──> spawn thread
└──> wait for database request
└──> answer request
request ──> spawn thread
└──> wait for database request
└──> answer request
Vì vậy, luồng dành phần lớn thời gian của họ bằng cách sử dụng CPU 0% chờ cơ sở dữ liệu trả về dữ liệu. Trong khi làm như vậy, họ phải phân bổ bộ nhớ cần thiết cho một luồng bao gồm một ngăn chương trình hoàn toàn riêng biệt cho mỗi luồng, v.v. Ngoài ra, họ sẽ phải bắt đầu một luồng mà không tốn kém như khi bắt đầu một quá trình đầy đủ vẫn chưa chính xác rẻ.
Vòng lặp sự kiện Singlethreaded
Vì chúng tôi dành phần lớn thời gian để sử dụng CPU 0%, tại sao không chạy một số mã khi chúng tôi không sử dụng CPU? Theo cách đó, mỗi yêu cầu vẫn sẽ có cùng thời lượng CPU như các ứng dụng đa luồng nhưng chúng ta không cần phải bắt đầu một chuỗi. Vì vậy, chúng tôi làm điều này:
request ──> make database request
request ──> make database request
request ──> make database request
database request complete ──> send response
database request complete ──> send response
database request complete ──> send response
Trong thực tế, cả hai phương pháp đều trả về dữ liệu với độ trễ gần như nhau vì đó là thời gian đáp ứng cơ sở dữ liệu chi phối quá trình xử lý.
Ưu điểm chính ở đây là chúng ta không cần phải tạo ra một chủ đề mới vì vậy chúng ta không cần phải làm nhiều và rất nhiều malloc sẽ làm chúng ta chậm lại.
Phép thuật, luồng vô hình
Điều có vẻ bí ẩn là làm thế nào cả hai cách tiếp cận ở trên quản lý để chạy khối lượng công việc "song song"? Câu trả lời là cơ sở dữ liệu được xâu chuỗi. Vì vậy, ứng dụng đơn luồng của chúng tôi thực sự đang thúc đẩy hành vi đa luồng của một quy trình khác: cơ sở dữ liệu.
Trường hợp cách tiếp cận singlethreaded thất bại
Một ứng dụng singlethreaded thất bại lớn nếu bạn cần thực hiện nhiều phép tính CPU trước khi trả lại dữ liệu. Bây giờ, tôi không có nghĩa là một vòng lặp để xử lý kết quả cơ sở dữ liệu. Điều đó vẫn chủ yếu là O (n). Ý tôi là những thứ như thực hiện biến đổi Fourier (ví dụ mã hóa mp3), dò tia (kết xuất 3D), v.v.
Một cạm bẫy khác của các ứng dụng đơn lẻ là nó sẽ chỉ sử dụng một lõi CPU duy nhất. Vì vậy, nếu bạn có một máy chủ lõi tứ (không phổ biến ngày nay) thì bạn không sử dụng 3 lõi khác.
Trường hợp phương pháp đa luồng không thành công
Một ứng dụng đa luồng không thành công lớn nếu bạn cần phân bổ nhiều RAM cho mỗi luồng. Đầu tiên, bản thân việc sử dụng RAM có nghĩa là bạn không thể xử lý nhiều yêu cầu như một ứng dụng đã đọc. Tệ hơn, malloc là chậm. Phân bổ nhiều và rất nhiều đối tượng (vốn phổ biến cho các khung web hiện đại) có nghĩa là chúng ta có khả năng sẽ chậm hơn các ứng dụng đã đọc. Đây là nơi mà node.js thường giành chiến thắng.
Một trường hợp sử dụng cuối cùng làm cho đa luồng trở nên tồi tệ hơn là khi bạn cần chạy một ngôn ngữ kịch bản lệnh khác trong luồng của mình. Đầu tiên bạn thường cần malloc toàn bộ thời gian chạy cho ngôn ngữ đó, sau đó bạn cần malloc các biến được sử dụng bởi tập lệnh của bạn.
Vì vậy, nếu bạn đang viết các ứng dụng mạng bằng C hoặc go hoặc java thì chi phí phân luồng thường sẽ không quá tệ. Nếu bạn đang viết một máy chủ web C để phục vụ PHP hoặc Ruby thì việc viết một máy chủ nhanh hơn bằng javascript hoặc Ruby hoặc Python là rất dễ dàng.
Phương pháp lai
Một số máy chủ web sử dụng phương pháp lai. Ví dụ, Nginx và Apache2 triển khai mã xử lý mạng của họ như một chuỗi các vòng lặp sự kiện. Mỗi luồng chạy một vòng lặp sự kiện đồng thời xử lý các yêu cầu đơn luồng nhưng các yêu cầu được cân bằng tải giữa nhiều luồng.
Một số kiến trúc đơn luồng cũng sử dụng phương pháp lai. Thay vì khởi chạy nhiều luồng từ một tiến trình đơn lẻ, bạn có thể khởi chạy nhiều ứng dụng - ví dụ: 4 máy chủ node.js trên máy lõi tứ. Sau đó, bạn sử dụng một bộ cân bằng tải để phân tán khối lượng công việc giữa các quy trình.
Trong thực tế, hai cách tiếp cận là hình ảnh phản chiếu giống hệt nhau về mặt kỹ thuật của nhau.