blog.dopana

Back

Bài thứ mười lăm trong loạt bài Rust Intermediate. Xử lý đồng thời (concurrency) vốn là một trong những bài toán phức tạp và dễ phát sinh lỗi nhất trong lập trình — từ race condition, deadlock cho đến memory corruption.

Rust giải quyết vấn đề này với triết lý Fearless Concurrency (Lập trình đồng thời không sợ hãi): hệ thống quyền sở hữu (ownership) và hệ thống kiểu dữ liệu (type system) sẽ bắt và loại bỏ mọi lỗi an toàn bộ nhớ ngay từ thời điểm biên dịch.

graph TD
    A["Fearless Concurrency trong Rust"] --> B["Tạo luồng (Threads)<br/>std::thread::spawn"]
    A --> C["Truyền thông điệp (Message Passing)<br/>Channels (mpsc)"]
    A --> D["Chia sẻ trạng thái (Shared State)<br/>Arc<T> + Mutex<T>"]
    A --> E["Mở rộng an toàn (Extensible Safety)<br/>Traits Send & Sync"]

Giải thích đơn giản (ELI5): Nhà hàng có nhiều đầu bếp#

Hãy tưởng tượng một căn bếp nhà hàng:

  1. Đơn luồng (Single-thread): Chỉ có 1 đầu bếp làm mọi việc từ nhặt rau, nấu ăn đến rửa bát. Rất an toàn nhưng chậm.
  2. Đa luồng không an toàn (C/C++): 5 đầu bếp cùng xông vào cắt chung một củ hành mà không có luật lệ nào, dẫn đến tranh chấp dao thớt và dễ cắt vào tay nhau (race condition, data corruption).
  3. Rust Fearless Concurrency: Mỗi đầu bếp được phân chia khu vực rõ ràng (ownership), nói chuyện qua đường băng chuyền order (channels), hoặc nếu muốn dùng chung một cái chảo, họ phải có chìa khóa phòng bếp và ổ khóa độc quyền (Arc + Mutex). Trình biên dịch là bếp trưởng kiểm tra nghiêm ngặt mọi quy trình trước khi mở cửa!

1. Tạo luồng chạy song song (Threads)#

Trong Rust, bạn sử dụng thread::spawn để tạo một hệ điều hành luồng (native OS thread):

Chuyển quyền sở hữu vào luồng với move#

Khi luồng con cần truy cập dữ liệu từ luồng cha, bạn sử dụng từ khóa move để chuyển giao ownership:

src/main.rs
use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Vector trong luồng mới: {v:?}");
    });

    handle.join().unwrap();
}
rust

2. Truyền thông điệp qua Channel (Message Passing)#

Khẩu hiệu nổi tiếng của ngôn ngữ Go cũng được áp dụng hoàn hảo trong Rust: “Đừng giao tiếp bằng cách chia sẻ bộ nhớ; thay vào đó, hãy chia sẻ bộ nhớ bằng cách giao tiếp.”

Rust cung cấp mpsc (multiple producer, single consumer — nhiều bên gửi, một bên nhận):

3. Chia sẻ trạng thái: Arc<T>Mutex<T>#

Khi các luồng thực sự cần cùng đọc và ghi vào một vùng bộ nhớ:

  • Mutex<T> (Mutual Exclusion): Đảm bảo tại một thời điểm chỉ có duy nhất 1 luồng được truy cập dữ liệu thông qua khóa lock().
  • Arc<T> (Atomic Reference Counting): Con trỏ đếm tham chiếu an toàn luồng, cho phép nhiều luồng cùng sở hữu dữ liệu.

[!NOTE] Rc<T> không an toàn cho đa luồng vì việc tăng giảm biến đếm không mang tính atomic (nguyên tử). Do đó, bạn bắt buộc phải dùng Arc<T> thay cho Rc<T> khi làm việc với nhiều luồng.

4. Hai Trait cốt lõi: SendSync#

Hệ thống concurrency của Rust không bị “hard-code” trong ngôn ngữ mà được xây dựng trên 2 auto traits:

  1. Send: Chỉ ra rằng quyền sở hữu của kiểu dữ liệu này có thể được chuyển giao an toàn giữa các luồng.
  2. Sync: Chỉ ra rằng việc nhiều luồng cùng tham chiếu (&T) tới kiểu dữ liệu này là an toàn (T: Sync tương đương &T: Send).

Hầu hết các kiểu nguyên thủy trong Rust đều tự động triển khai SendSync. Trình biên dịch sẽ ngăn chặn ngay lập tức nếu bạn cố tình gửi một kiểu không an toàn (như Rc<T>) qua luồng khác.

Tóm tắt#

  • thread::spawn tạo native OS thread, kết hợp với move closures để chuyển giao dữ liệu an toàn.
  • mpsc::channel cung cấp kênh truyền thông điệp an toàn giữa các producers và consumer.
  • Arc<Mutex<T>> là công thức chuẩn mực để chia sẻ và biến đổi trạng thái đồng thời giữa nhiều luồng.
  • Các traits SendSync biến sự an toàn đồng thời thành quy tắc kiểm tra kiểu ở cấp độ biên dịch.

Tài liệu tham khảo#