Lập trình đồng thời an toàn trong Rust
Khám phá Fearless Concurrency trong Rust: sử dụng thread, truyền thông điệp qua channel, chia sẻ trạng thái với Arc và Mutex, an toàn từ lúc biên dịch.
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:
- Đơ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.
- Đ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).
- 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):
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..=5 {
println!("Luồng phụ (spawned thread): {i}");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..=3 {
println!("Luồng chính (main thread): {i}");
thread::sleep(Duration::from_millis(1));
}
// Đợi luồng phụ kết thúc
handle.join().unwrap();
}rustChuyể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:
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();
}rust2. 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):
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone(); // Tạo thêm một producer
// Luồng gửi 1
thread::spawn(move || {
let msgs = vec!["xin", "chào", "từ", "luồng 1"];
for msg in msgs {
tx1.send(String::from(msg)).unwrap();
thread::sleep(Duration::from_millis(200));
}
});
// Luồng gửi 2
thread::spawn(move || {
let msgs = vec!["dữ", "liệu", "từ", "luồng 2"];
for msg in msgs {
tx.send(String::from(msg)).unwrap();
thread::sleep(Duration::from_millis(200));
}
});
// Luồng chính nhận thông điệp
for received in rx {
println!("Nhận được: {received}");
}
}rust3. Chia sẻ trạng thái: Arc<T> và 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óalock().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.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Kết quả đếm cuối cùng: {}", *counter.lock().unwrap()); // 10
}rust[!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ùngArc<T>thay choRc<T>khi làm việc với nhiều luồng.
4. Hai Trait cốt lõi: Send và Sync#
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:
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.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: Synctương đương&T: Send).
Hầu hết các kiểu nguyên thủy trong Rust đều tự động triển khai Send và Sync. 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::spawntạo native OS thread, kết hợp vớimoveclosures để chuyển giao dữ liệu an toàn.mpsc::channelcung 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
SendvàSyncbiến sự an toàn đồng thời thành quy tắc kiểm tra kiểu ở cấp độ biên dịch.