Trong bài trước, ta đã tìm hiểu về bất đồng bộ và song song một cách rõ ràng, hai khái niệm này độc lập, bổ sung cho nhau. Chúng ta cũng có đề cập đến khái niệm luồng và tiến trình xử lý. Ta cũng hiểu rằng trong một tiến trình có thể xử lý nhiều luồng, tuy rằng bản chất chúng không song song. Ở bài này ta sẽ đi sâu hơn về đa luồng (multi-threading) cùng với việc xử lý bất đồng bộ trên một số ngôn ngữ đơn luồng như PHP, nodejs.
Đa luồng là gì?
Mỗi tiến trình (process) được cấp phát bộ nhớ và tài nguyên của riêng mình. Việc chuyển đổi giữa các tiến trình cũng mất nhiều công đoạn hơn. Do đó, để tận dụng tối ưu tài nguyên, ta cần tìm cách cho phép làm được nhiều tác vụ cùng lúc trong cùng một tiến trình.
Đa luồng là một mô hình thực thi chương trình cho phép tạo nhiều luồng (threads) trong một tiến trình, thực thi độc lập nhưng đồng thời chia sẻ tài nguyên của cùng một tiến trình (process). Tùy thuộc vào phần cứng, các luồng độc lập có thể được xử lý gần như song song trên cùng một lõi CPU.
Lý do chính để kết hợp các luồng vào một tiến trình là để cải thiện hiệu suất của nó, tận dụng được hết năng lực xử lý của CPU. Hiệu suất có thể được thể hiện theo nhiều cách:
- Một máy chủ web sẽ sử dụng nhiều luồng để xử lý đồng thời các yêu cầu dữ liệu cùng một lúc.
- Một thuật toán phân tích hình ảnh sẽ sinh ra nhiều luồng cùng một lúc và phân chia hình ảnh thành các góc phần tư để áp dụng tính năng lọc cho hình ảnh.
Đa luồng cũng giúp giảm thiểu và sử dụng hiệu quả hơn các tài nguyên máy tính. Khả năng đáp ứng của ứng dụng được cải thiện khi các yêu cầu từ một luồng không chặn các yêu cầu từ các luồng khác.
Một số ví dụ
Hầu hết các ứng dụng mà bạn sử dụng hàng ngày đều có nhiều luồng chạy ngầm. Hãy xem xét trình duyệt Chrome/Firefox của bạn. Tại bất kỳ thời điểm nào, bạn có thể mở nhiều tab, mỗi tab hiển thị nhiều loại nội dung khác nhau. Nhiều luồng thực thi được sử dụng để tải nội dung, hiển thị hoạt ảnh, phát video, v.v.
Một ví dụ khác về chương trình đa luồng mà tất cả chúng ta đều quen thuộc là trình xử lý văn bản. Trong khi bạn đang nhập, nhiều chuỗi được sử dụng để hiển thị tài liệu của bạn, kiểm tra chính tả và ngữ pháp của tài liệu một cách không đồng bộ, tạo phiên bản PDF của tài liệu. Tất cả những điều này xảy ra đồng thời, với các luồng độc lập thực hiện các tác vụ này trong nội bộ.
Chúng ta cũng có thể so sánh hai máy chủ web phổ biến: Apache và Nginx. Nginx không đồng bộ và dựa trên sự kiện (event-base), trong khi Apache sử dụng đa luồng.
- Apache tạo các luồng mới cho mọi kết nối bổ sung, do đó, có số lượng kết nối được phép tối đa tùy thuộc vào bộ nhớ khả dụng trong hệ thống. Khi đạt đến giới hạn kết nối này, Apache sẽ từ chối các kết nối bổ sung. Yếu tố hạn chế trong việc điều chỉnh Apache là bộ nhớ (hãy nhớ rằng việc thực thi song song thường phụ thuộc vào phần cứng). Nếu một luồng dừng, máy khách sẽ đợi phản hồi cho đến khi luồng trở nên rảnh rỗi và do đó, nó có thể gửi phản hồi.
- Nginx hoạt động khác với Apache và nó không tạo luồng mới cho mỗi yêu cầu đến. Nó có một tiến trình chính chạy đơn luồng. Tiến trình này có thể xử lý hàng ngàn kết nối đồng thời. Nó thực hiện điều này không đồng bộ với một luồng, thay vì sử dụng thực thi song song đa luồng. Ta sẽ xem xét mô hình tương tự ở ví dụ với PHP sau.
Ngôn ngữ hỗ trợ đa luồng
Ở các ngôn ngữ hỗ trợ đa luồng, chương trình có thể được tách ra và xử lý cùng lúc trên nhiều luồng riêng biệt
Như vậy, việc triển khai bất đồng bộ trên các ngôn ngữ này cũng diễn ra thuần túy trên các luồng khác nhau như hình phía trên.
Single-threaded concurrency – đồng thời đơn luồng
PHP là ngôn ngữ lập trình đơn luồng (single thread). Đơn luồng có nghĩa là chỉ có một dòng mã PHP có thể được thực thi tại cùng một thời điểm. Vậy nghĩa là, PHP không thực sự bất đồng bộ! Một đoạn mã PHP không thể tách một phần công việc sang cho 1 luồng khác xử lý rồi chờ kết quả nhận được sau đó.
Do đó, bất đồng bộ với PHP chỉ được khắc phục với các trường hợp gọi các tác vụ I/O (gọi API, call database,..)
Với PHP “tuần tự” truyền thống không thể xử lý các lệnh gọi lại này. Ví dụ: chúng tôi muốn thực hiện hai yêu cầu HTTP đồng thời trong PHP:
<?php $client = new Browser(); $result1 = $client->get('http://google.com/'); $result2 = $client->get('https://github.com/reactphp');
PHP đơn luồng được thực thi từng dòng một. Để gọi request thứ hai ta phải chờ thực hiện xong request thứ nhất.
Vấn đề đều có thể được giải quyết bằng vòng lặp sự kiện. Ví dụ trước có thể được viết lại theo cách sau:
<?php $printResponse = fn (ResponseInterface $response) => var_dump((string)$response->getBody()); $promise1 = $client->get('http://google.com/'); $promise2 = $client->get('https://github.com/reactphp'); $promise1->then($printResponse); $promise2->then($printResponse);
Ở dòng cuối cùng, chương trình không thoát mà bắt đầu nghe các sự kiện.
Vòng lặp sự kiện (event loop) ở cuối là một vòng lặp “vô tận” lắng nghe các sự kiện cụ thể và gọi hàm xử lý cho chúng. Chương trình gọi 2 tác vụ không chặn I/O (requests) và yêu cầu HĐH thực hiện. Sau đó, luồng thực thi có thể làm điều gì đó khác và không đợi cho đến khi chúng hoàn thành. Khi hệ điều hành đã nhận được phản hồi của mạng, nó sẽ gửi cho chương trình một sự kiện với dữ liệu đã nhận được. Một bản ghi của sự kiện này được thêm vào hàng đợi sự kiện. Vòng lặp thực thi lấy sự kiện đầu tiên từ hàng đợi và gọi trình xử lý tương ứng cho sự kiện này.
.