Hiểu sâu về Tiến trình, Luồng và Coroutines trong Hệ điều hành

Từ bài toán sử dụng tài nguyên đến cơ chế đa nhiệm

Khi nói đến quản lý tài nguyên hệ thống, cốt lõi nằm ở cách hệ điều hành tận dụng tối đa phần cứng như CPU, bộ nhớ, thiết bị nhập xuất. Trở lại thời kỳ máy tính lớn như IBM 7094, một phương pháp đơn giản được áp dụng: xử lý theo lô (batch processing). Người dùng ghi chương trình lên băng từ, máy tính lần lượt thực thi từng tác vụ mà không có cơ chế ngắt hay chuyển đổi linh hoạt.

Vấn đề nảy sinh khi gặp các tác vụ phụ thuộc vào nhập/xuất (I/O-bound). Ví dụ đoạn mã C sau:

#include <stdio.h>

int main() {
    int value = 0;
    scanf("%d", &value);  // CPU dừng chờ người dùng nhập liệu
    return 0;
}

Tại điểm gọi scanf, CPU hoàn toàn "đóng băng" cho đến khi dữ liệu được nhập — một sự lãng phí tài nguyên nghiêm trọng. Để khắc phục, các nhà khoa học máy tính đã phát triển cơ chế đa chương (multiprogramming): thay vì để CPU rảnh, hệ thống sẽ tạm dừng tiến trình đang chờ I/O và chuyển sang thực thi một tiến trình khác có thể sử dụng CPU.

Đây chính là nền tảng của khái niệm đồng thời (concurrency) — mặc dù chỉ có một CPU vật lý, nhưng nhờ luân phiên nhanh giữa các tác vụ, hệ thống tạo cảm giác như nhiều chương trình đang chạy song song.

Tiến trình: Đơn vị thực thi độc lập

Để quản lý việc chuyển đổi giữa các chương trình, hệ điều hành cần lưu trạng thái hiện tại của từng tiến trình. Dữ liệu này được tổ chức trong một cấu trúc gọi là PCB (Process Control Block), bao gồm:

  • Con trỏ đến mã lệnh (PC - Program Counter)
  • Trạng thái thanh ghi
  • Không gian địa chỉ ảo
  • Thông tin tài nguyên đang giữ
  • Trạng thái hiện tại (mới tạo, sẵn sàng, đang chạy, chờ, kết thúc)

Mỗi chương trình đang chạy được gọi là một tiến trình (process). Đây là một thực thể độc lập với không gian bộ nhớ riêng biệt, được bảo vệ bởi cơ chế bộ nhớ ảo (virtual memory). Nhờ bảng ánh xạ trang (page table), hai tiến trình có thể cùng tham chiếu tới địa chỉ ảo 100 nhưng thực tế trỏ tới hai vùng vật lý khác nhau — ngăn chặn xung đột dữ liệu.

Khi chuyển đổi tiến trình (context switch), hệ điều hành phải:

  1. Lưu toàn bộ trạng thái hiện tại vào PCB của tiến trình nguồn
  2. Nạp trạng thái từ PCB của tiến trình đích
  3. Cập nhật bảng ánh xạ bộ nhớ ảo

Thao tác này tốn kém về mặt hiệu năng do liên quan đến cả phần cứng và nhân hệ điều hành.

Luồng: Chia sẻ tài nguyên bên trong tiến trình

Do chi phí chuyển đổi tiến trình cao, người ta cần một cơ chế nhẹ hơn cho phép đồng thời trong phạm vi một tiến trình. Từ đó ra đời khái niệm luồng (thread).

Một tiến trình có thể chứa nhiều luồng. Các luồng trong cùng tiến trình:

  • Chia sẻ không gian bộ nhớ ảo
  • Chia sẻ tài nguyên mở tập tin, biến toàn cục
  • Có thanh ghi và ngăn xếp riêng

Việc chuyển đổi giữa các luồng thuộc cùng tiến trình nhanh hơn nhiều so với chuyển tiến trình, vì không cần thay đổi bảng ánh xạ bộ nhớ. Mỗi luồng có một TCB (Thread Control Block) riêng để lưu trạng thái thực thi.

Quan trọng hơn, luồng là thực thể được hệ điều hành nhận diện — còn gọi là luồng cấp hạt nhân (kernel-level thread). Khi một luồng bị khóa do gọi hệ thống (system call), hệ điều hành vẫn có thể lập lịch cho các luồng khác trong cùng hoặc tiến trình khác tiếp tục thực thi, đảm bảo hiệu suất CPU luôn cao.

Coroutines: Đồng thời cấp người dùng

Trong khi luồng phụ thuộc vào hệ điều hành, coroutine là cơ chế đồng thời được triển khai hoàn toàn ở cấp người dùng (user-level). Không giống luồng, coroutine không được hệ điều hành biết đến — chúng chỉ là các hàm có thể tạm dừng và tiếp tục thực thi tại điểm dừng cũ.

Xét ví dụ mô phỏng coroutine bằng cơ chế ngăn xếp riêng:

struct Coroutine {
    void* stack_ptr;      // Con trỏ ngăn xếp
    void* instruction_ptr; // Điểm thực thi tiếp theo
};

void yield(struct Coroutine* next) {
    save_current_context(&current->stack_ptr);
    switch_stack_pointer(next->stack_ptr);
    jump_to(next->instruction_ptr);
}

Tại đây, yield() đóng vai trò chuyển ngữ cảnh giữa hai coroutine bằng cách lưu và đổi con trỏ ngăn xếp — hoàn toàn trong không gian người dùng, không cần system call.

Ưu điểm: cực nhẹ, chi phí chuyển đổi rất thấp, lập lịch tùy chỉnh (cooperative scheduling). Tuy nhiên, nhược điểm lớn là nếu một coroutine thực hiện thao tác blocking (như đọc file), toàn bộ tiến trình sẽ bị khóa — vì hệ điều hành chỉ thấy một luồng duy nhất đang chạy.

So sánh và ứng dụng thực tế

Tiêu chí Tiến trình Luồng Coroutine
Không gian bộ nhớ Riêng biệt Chia sẻ Chia sẻ
Chi phí chuyển đổi Cao Trung bình Thấp
Khả năng xử lý blocking I/O Tốt Tốt Kém
Được hệ điều hành quản lý Không

Ngày nay, nhiều ngôn ngữ hỗ trợ coroutine như Python (async/await), Kotlin (kotlinx.coroutines), Java (Virtual Threads từ JDK 19+). Những giải pháp này kết hợp ưu điểm của coroutine (nhẹ) và luồng (không bị blocking) thông qua mô hình project loom — ánh xạ hàng ngàn virtual thread lên ít kernel thread thật sự.

Thẻ: operating-system Concurrency process thread coroutine

Đăng vào ngày 14 tháng 6 lúc 06:12