Trạng thái dùng chung trong Rust: RwLock, atomic, scoped thread
Ở trang Lập trình đồng thời, bạn đã chia sẻ một bộ đếm giữa các luồng (thread) bằng Arc<Mutex<T>>: Arc cho phép nhiều luồng cùng sở hữu giá trị, còn Mutex chỉ cho một luồng đụng vào nó tại một thời điểm. Ở trang Channel, bạn tránh hẳn việc chia sẻ bằng cách gửi tin nhắn. Cả hai đều là công cụ tuyệt vời, và bạn có thể xây gần như mọi thứ với chúng.
Nhưng hãy nghĩ về Mutex như chiếc chìa khóa duy nhất của phòng vệ sinh văn phòng. Nó hoạt động, nhưng hơi quá tay nếu tất cả những gì bạn muốn là đọc bảng thông báo ngoài hành lang. Thư viện chuẩn của Rust còn vài công cụ khác, mỗi cái hợp hơn cho một việc nhất định. Ở trang này bạn sẽ gặp bốn công cụ: scoped thread, RwLock, atomic và OnceLock/LazyLock. Bạn cũng sẽ học cách tránh loại lỗi duy nhất mà trình biên dịch không bắt được giúp bạn: deadlock (khóa chết).
Scoped thread: mượn thay vì move
Phần tiêu đề “Scoped thread: mượn thay vì move”Với thread::spawn, bạn thường cần move và hay cần cả Arc. Đó là vì một luồng được tạo ra có thể vẫn chạy tiếp sau khi main đã đi tiếp, nên Rust không cho nó mượn các biến cục bộ của bạn.
std::thread::scope giải quyết chuyện này bằng một lời hứa: mọi luồng được khởi chạy bên trong scope đều kết thúc trước khi scope kết thúc. Vì các luồng không thể sống lâu hơn dữ liệu của bạn, chúng được phép mượn nó một cách đơn giản. Không Arc, không move, không clone.
use std::thread;
fn main() { let shop = "Tiệm góc phố"; let sales = vec![12, 7, 30, 5, 18, 22, 9, 14]; let (morning, afternoon) = sales.split_at(4);
let (am, pm) = thread::scope(|s| { let am = s.spawn(|| morning.iter().sum::<i32>()); let pm = s.spawn(|| afternoon.iter().sum::<i32>()); (am.join().unwrap(), pm.join().unwrap()) });
println!("{shop}: buổi sáng {am}, buổi chiều {pm}, cả ngày {}", am + pm); println!("vẫn là của tôi: {:?}", sales);}Kết quả:
Tiệm góc phố: buổi sáng 54, buổi chiều 63, cả ngày 117vẫn là của tôi: [12, 7, 30, 5, 18, 22, 9, 14]Điều đang diễn ra:
thread::scope(|s| { ... })mở scope. Bạn khởi chạy luồng bằngs.spawn(...)thay vìthread::spawn(...).- Hai closure mượn
morningvàafternoon, vốn chỉ là các slice củasales. Không có gì bị sao chép. - Thứ mà closure của scope trả về (ở đây là một tuple gồm hai tổng) trở thành giá trị của
thread::scope. - Sau scope,
salesvẫn thuộc vềmain, nên bạn có thể tiếp tục dùng nó.
Kể cả khi bạn quên gọi join(), scope vẫn chờ mọi luồng của nó ở dấu } đóng. Đó chính là lời hứa giúp việc mượn trở nên an toàn.
RwLock: nhiều người đọc hoặc một người ghi
Phần tiêu đề “RwLock: nhiều người đọc hoặc một người ghi”Hãy hình dung tấm bảng menu trong một quán cà phê. Nhiều khách có thể đọc nó cùng lúc, chẳng sao cả. Nhưng khi chủ quán đổi giá, mọi người phải chờ một chút cho tới khi bảng mới được treo lên.
Đó là một RwLock (“read-write lock”, khóa đọc-ghi). Nó có hai lối vào:
read()cho bạn một guard đọc dùng chung. Nhiều luồng có thể giữ nó cùng lúc.write()cho bạn một guard ghi độc quyền. Trong khi một người ghi đang giữ nó, không ai khác đọc hay ghi được.
Mutex không phân biệt điều đó: ngay cả hai người đọc cũng phải lần lượt. Đây là một cache cài đặt mà vài bộ xử lý yêu cầu cùng đọc, và một quản trị viên cập nhật một lần:
use std::sync::{Arc, RwLock};use std::thread;
struct Settings { theme: String, max_upload_mb: u32,}
fn main() { let settings = Arc::new(RwLock::new(Settings { theme: String::from("light"), max_upload_mb: 10, }));
// Bốn bộ xử lý yêu cầu đọc cài đặt cùng lúc. let mut handles = Vec::new(); for id in 1..=4 { let settings = Arc::clone(&settings); handles.push(thread::spawn(move || { let s = settings.read().unwrap(); // dùng chung: người khác cũng đọc được format!("yêu cầu {id}: giao diện={}, giới hạn={}MB", s.theme, s.max_upload_mb) })); } for handle in handles { println!("{}", handle.join().unwrap()); }
// Một quản trị viên đổi cài đặt một lần. { let mut s = settings.write().unwrap(); // độc quyền: mọi người khác phải chờ s.theme = String::from("dark"); s.max_upload_mb = 25; } // guard ghi bị drop ở đây, nên khóa lại được giải phóng
let s = settings.read().unwrap(); println!("sau khi cập nhật: giao diện={}, giới hạn={}MB", s.theme, s.max_upload_mb);}Kết quả:
yêu cầu 1: giao diện=light, giới hạn=10MByêu cầu 2: giao diện=light, giới hạn=10MByêu cầu 3: giao diện=light, giới hạn=10MByêu cầu 4: giao diện=light, giới hạn=10MBsau khi cập nhật: giao diện=dark, giới hạn=25MBCác luồng có thể xong theo thứ tự bất kỳ, nhưng kết quả lúc nào cũng giống nhau. Mỗi luồng trả về dòng của nó thay vì tự in ra, và main in chúng theo thứ tự nó join.
Khi nào RwLock hơn Mutex? Khi việc đọc diễn ra thường xuyên, việc ghi hiếm khi xảy ra, và mỗi lần đọc mất một chút thời gian (chẳng hạn tìm trong một cache lớn). Khi đó những người đọc thật sự chạy song song với nhau.
Khi nào thì không? Nếu mỗi lần đọc rất nhỏ, hoặc việc ghi diễn ra liên tục, thì RwLock chỉ là một Mutex phức tạp hơn. Nó phải làm thêm việc sổ sách, và người đọc với người ghi phần lớn vẫn phải chờ nhau. Khi không chắc, hãy bắt đầu với Mutex. Nó đơn giản hơn, và bạn có thể đổi sau nếu đo đạc cho thấy khóa là điểm nghẽn.
Atomic: bộ đếm không cần khóa
Phần tiêu đề “Atomic: bộ đếm không cần khóa”Với một con số đơn lẻ hoặc một cờ đúng/sai, ngay cả một cái khóa cũng là quá mức cần thiết. Các kiểu atomic như AtomicUsize, AtomicI64 và AtomicBool có thể được nhiều luồng thay đổi an toàn cùng lúc mà không cần khóa nào. CPU khiến mỗi thao tác diễn ra trong một bước không thể chia nhỏ, nên không lần cập nhật nào bị mất.
Ở đây, 8 luồng mỗi luồng ghi nhận 1000 lượt xem trang. Chúng cũng đếm phiếu bầu: một phiếu thuận, rồi ba phiếu chống, lặp đi lặp lại. Tổng phiếu sẽ âm, đó là lý do nó là AtomicI64 chứ không phải một kiểu không dấu:
use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};use std::thread;
fn main() { let page_views = AtomicUsize::new(0); let net_votes = AtomicI64::new(0);
thread::scope(|s| { for _ in 0..8 { s.spawn(|| { for n in 0..1000 { page_views.fetch_add(1, Ordering::Relaxed); if n % 4 == 0 { net_votes.fetch_add(1, Ordering::Relaxed); // phiếu thuận } else { net_votes.fetch_sub(1, Ordering::Relaxed); // phiếu chống } } }); } }); // cả 8 luồng đều đã xong ở đây
println!("lượt xem trang: {}", page_views.load(Ordering::Relaxed)); println!("tổng phiếu: {}", net_votes.load(Ordering::Relaxed));}Kết quả:
lượt xem trang: 8000tổng phiếu: -4000Để ý rằng page_views không phải mut. Atomic thay đổi qua một tham chiếu dùng chung, đó chính là lý do nhiều luồng dùng được chúng cùng lúc. Các phương thức bạn sẽ dùng nhiều nhất:
fetch_add(n, ordering)/fetch_sub(n, ordering)cộng hoặc trừ, và trả lại giá trị cũ.load(ordering)đọc giá trị hiện tại.store(value, ordering)ghi đè lên nó.
Ordering là gì?
Phần tiêu đề “Ordering là gì?”Mỗi thao tác atomic đều yêu cầu một Ordering. Nó cho CPU và trình biên dịch biết thao tác này phải giữ đúng thứ tự với các thao tác bộ nhớ xung quanh chặt chẽ tới mức nào. Câu chuyện đầy đủ khá sâu, nhưng là người mới, bạn chỉ cần hai quy tắc:
- Khi không chắc, hãy dùng
Ordering::SeqCst. Đó là lựa chọn chặt chẽ và an toàn nhất. Ordering::Relaxedlà đủ cho một bộ đếm đơn giản mà bạn chỉ đọc ở cuối, như ví dụ trên. Tổng vẫn hoàn toàn chính xác.
Nếu một ngày nào đó bạn muốn đi xa hơn, cuốn sách miễn phí Rust Atomics and Locks (có link ở cuối trang) giải thích các ordering rất rõ ràng.
Một cờ dừng
Phần tiêu đề “Một cờ dừng”Một AtomicBool là tín hiệu “làm ơn dừng lại” rất tốt. Ở đây một worker chạy nền tiếp tục tự động lưu cho tới khi main bật cờ:
use std::sync::atomic::{AtomicBool, Ordering};use std::thread;use std::time::Duration;
fn main() { let stop = AtomicBool::new(false);
let report = thread::scope(|s| { let worker = s.spawn(|| { while !stop.load(Ordering::SeqCst) { // giả vờ tự động lưu tài liệu ở đây thread::sleep(Duration::from_millis(5)); } "worker tự động lưu đã dừng gọn gàng" });
thread::sleep(Duration::from_millis(30)); // main làm việc khác stop.store(true, Ordering::SeqCst); // yêu cầu worker dừng lại worker.join().unwrap() });
println!("{report}");}Kết quả:
worker tự động lưu đã dừng gọn gàngWorker kiểm tra cờ mỗi vòng lặp. Nó làm xong bước hiện tại rồi tự thoát, nhẹ nhàng hơn nhiều so với việc giết một luồng khi nó đang lưu tệp dở dang.
OnceLock và LazyLock: thiết lập một lần
Phần tiêu đề “OnceLock và LazyLock: thiết lập một lần”Đôi khi bạn muốn một giá trị toàn cục mà mọi luồng đều đọc được: một bảng tra cứu, tên ứng dụng, một cấu hình được nạp lúc khởi động. Phần khó là tạo ra nó. Nếu hai luồng cùng cố tạo nó vào đúng một thời điểm thì sao?
std::sync::LazyLock và std::sync::OnceLock giải quyết chuyện này. Giá trị được tạo đúng một lần, vào lần đầu tiên có ai đó cần nó. Nếu các luồng khác hỏi trong lúc nó đang được tạo, chúng chỉ việc chờ, rồi mọi người cùng dùng chung một giá trị.
use std::collections::HashMap;use std::sync::{LazyLock, OnceLock};use std::thread;
// LazyLock: công thức được viết ngay tại đây, và chạy ở lần dùng đầu tiên.static CURRENCIES: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| { println!("đang dựng bảng tiền tệ..."); HashMap::from([("US", "USD"), ("JP", "JPY"), ("DE", "EUR"), ("BR", "BRL")])});
// OnceLock: bắt đầu rỗng, và bạn quyết định cách điền nó ở nơi bạn dùng.static STORE_NAME: OnceLock<String> = OnceLock::new();
fn store_name() -> &'static str { STORE_NAME.get_or_init(|| { std::env::var("STORE_NAME").unwrap_or_else(|_| String::from("Rusty Shop")) })}
fn main() { let mut handles = Vec::new(); for country in ["US", "JP", "DE", "BR"] { handles.push(thread::spawn(move || { format!("{} ở {country} tính tiền bằng {}", store_name(), CURRENCIES[country]) })); } for handle in handles { println!("{}", handle.join().unwrap()); }}Kết quả:
đang dựng bảng tiền tệ...Rusty Shop ở US tính tiền bằng USDRusty Shop ở JP tính tiền bằng JPYRusty Shop ở DE tính tiền bằng EURRusty Shop ở BR tính tiền bằng BRLBốn luồng cùng muốn có bảng vào khoảng cùng một lúc, vậy mà “đang dựng bảng tiền tệ…” chỉ được in một lần. Và vì đây là các giá trị static sống suốt chương trình, các luồng hoàn toàn không cần Arc.
Nên chọn cái nào?
LazyLockkhi bạn biết cách tạo giá trị ngay tại chỗ khai báo. Nó có cảm giác như một biến toàn cục bình thường.OnceLockkhi giá trị được quyết định sau, ví dụ từ một cờ dòng lệnh. Bạn điền nó bằngget_or_init(...)hoặcset(...)(vàsetsẽ thất bại một cách lịch sự nếu nó đã được điền).
Cả hai đều thuộc thư viện chuẩn. LazyLock xuất hiện từ Rust 1.80, nên code cũ hơn thường dùng crate once_cell hoặc lazy_static cho cùng việc này.
Tránh deadlock
Phần tiêu đề “Tránh deadlock”Trình biên dịch ngăn được data race, nhưng có một lỗi đồng thời mà nó không bắt được: deadlock. Đó là khi các luồng chờ nhau mãi mãi và chương trình của bạn cứ thế đứng im. Không lỗi, không sập, không gì cả.
Công thức kinh điển là hai cái khóa được lấy theo thứ tự khác nhau. Hãy tưởng tượng hai người, mỗi người đều cần cả cây bút lẫn cuốn sổ. Một người cầm lấy bút, người kia cầm lấy sổ, và mỗi người lịch sự chờ người kia buông ra. Mãi mãi.
// Luồng A // Luồng Blet from = alice.lock().unwrap(); let from = bob.lock().unwrap();let to = bob.lock().unwrap(); let to = alice.lock().unwrap();// A giữ alice, chờ bob. // B giữ bob, chờ alice. Kẹt cứng!Ba thói quen giúp bạn an toàn:
- Luôn lấy khóa theo cùng một thứ tự. Nếu mọi luồng đều khóa
alicetrướcbob(ví dụ, sắp theo mã tài khoản), vòng chờ không thể xảy ra. - Giữ phạm vi khóa ngắn. Chỉ giữ khóa đủ lâu để đọc hoặc sửa dữ liệu. Đừng bao giờ giữ nó trong lúc làm việc chậm như gọi mạng, ghi tệp hay một phép tính dài. Mọi luồng khác sẽ bị kẹt chờ suốt thời gian đó.
- Drop guard sớm. Một guard mở khóa khi nó bị drop. Bạn có thể kết thúc nó bằng một khối bên trong
{ ... }(như người ghiRwLockở trên) hoặc tự gọidrop(guard).
Đây là thói quen thứ 3 trong thực tế. Mỗi worker lấy một việc từ hàng đợi chung, mở khóa hàng đợi, và chỉ sau đó mới làm phần chậm. (Tên công việc được giữ bằng tiếng Anh vì kết quả được sắp xếp theo chữ cái.)
use std::sync::Mutex;use std::thread;
fn main() { let queue = Mutex::new(vec!["resize photo", "send email", "build report"]); let finished = Mutex::new(Vec::new());
thread::scope(|s| { for _ in 0..3 { s.spawn(|| { let mut jobs = queue.lock().unwrap(); let job = jobs.pop(); drop(jobs); // mở khóa hàng đợi TRƯỚC phần chậm
if let Some(job) = job { let result = format!("done: {job}"); // hãy tưởng tượng việc này mất vài giây finished.lock().unwrap().push(result); } }); } });
// Scope đã kết thúc, nên không luồng nào dùng `finished` nữa. let mut done = finished.into_inner().unwrap(); done.sort(); // các luồng xong theo thứ tự bất kỳ for line in done { println!("{line}"); }}Kết quả:
done: build reportdone: resize photodone: send emailKhông có drop(jobs), guard sẽ sống tới cuối closure. Khi đó phần việc “chậm” sẽ diễn ra trong lúc hàng đợi vẫn bị khóa, và ba worker sẽ chạy lần lượt từng cái thay vì song song.
Có một trường hợp khó thấy cần biết: trong while let Some(job) = queue.lock().unwrap().pop() { ... }, guard sống suốt toàn bộ thân vòng lặp. Vì vậy hàng đợi vẫn bị khóa trong lúc bạn xử lý công việc. Hãy lấy công việc ra trong một câu lệnh let riêng trước, như ví dụ đã làm.
Nên dùng công cụ nào?
Phần tiêu đề “Nên dùng công cụ nào?”| Bạn muốn… | Hãy dùng |
|---|---|
| Gửi công việc hoặc kết quả từ luồng này sang luồng khác | Một channel (mpsc) |
| Cho nhiều luồng cùng đọc và sửa một giá trị | Arc<Mutex<T>> |
| Chia sẻ dữ liệu được đọc nhiều và ít khi thay đổi | Arc<RwLock<T>> |
| Đếm, hoặc chia sẻ một cờ bật/tắt đơn giản | Atomic (AtomicUsize, AtomicBool, …) |
| Tạo một giá trị toàn cục một lần, rồi chỉ đọc nó | LazyLock hoặc OnceLock |
Cho các luồng mượn dữ liệu cục bộ, không cần Arc |
thread::scope |
Những công cụ này kết hợp thoải mái với nhau. Các ví dụ trên trang này đã kết hợp thread::scope với atomic và với Mutex. Hãy chọn công cụ đơn giản nhất phù hợp, và đừng lo chọn “sai”: đổi lại sau thường chỉ là một thay đổi nhỏ.
Lỗi thường gặp
Phần tiêu đề “Lỗi thường gặp”Mượn một biến cục bộ trong thread::spawn. Đoạn này trông vô hại:
use std::thread;
fn main() { let sales = vec![12, 7, 30, 5];
let handle = thread::spawn(|| { let total: i32 = sales.iter().sum(); total });
println!("total: {}", handle.join().unwrap());}error[E0373]: closure may outlive the current function, but it borrows `sales`, which is owned by the current function --> src/main.rs:6:32 |6 | let handle = thread::spawn(|| { | ^^ may outlive borrowed value `sales`7 | let total: i32 = sales.iter().sum(); | ----- `sales` is borrowed here |help: to force the closure to take ownership of `sales` (and any other referenced variables), use the `move` keywordTrình biên dịch (bằng tiếng Anh) nói rằng closure có thể sống lâu hơn hàm hiện tại, trong khi nó lại mượn sales. Bạn biết mình gọi join() ngay sau đó, nhưng thread::spawn không thể biết điều này. Với nó, luồng có thể chạy mãi mãi. Bạn có hai cách sửa: thêm move (khi đó main không dùng được sales nữa), hoặc dùng thread::scope, thứ có thể mượn:
let total: i32 = thread::scope(|s| s.spawn(|| sales.iter().sum()).join().unwrap());Quên Arc. Không có Arc, closure move của luồng đầu tiên lấy mất Mutex, và không còn gì cho luồng tiếp theo:
use std::sync::Mutex;use std::thread;
fn main() { let counter = Mutex::new(0); let mut handles = vec![];
for _ in 0..3 { handles.push(thread::spawn(move || { *counter.lock().unwrap() += 1; })); }
for handle in handles { handle.join().unwrap(); }}error[E0382]: use of moved value: `counter` --> src/main.rs:9:36 | 5 | let counter = Mutex::new(0); | ------- move occurs because `counter` has type `std::sync::Mutex<i32>`, which does not implement the `Copy` trait... 8 | for _ in 0..3 { | ------------- inside of this loop 9 | handles.push(thread::spawn(move || { | ^^^^^^^ value moved into closure here, in previous iteration of loopThông báo nói rằng counter đã bị move vào closure ở lần lặp trước. Hãy bọc nó trong Arc::new(...) và clone Arc ở đầu vòng lặp, như ở trang Lập trình đồng thời. Hoặc dùng thread::scope và bỏ move. Nếu bạn dùng Rc thay cho Arc, bạn sẽ nhận được error[E0277]: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely. Rc chỉ dành cho một luồng (xem Con trỏ thông minh).
Luyện tập
Phần tiêu đề “Luyện tập”1. Cộng theo bốn phần
Phần tiêu đề “1. Cộng theo bốn phần”Tạo một Vec gồm các số từ 1 đến 1000. Dùng thread::scope để chia nó thành 4 phần, mỗi phần 250 số, và cộng mỗi phần trong một luồng riêng. In tổng của từng phần và tổng cộng. Gợi ý: numbers.chunks(250) cho bạn các slice. Gom các handle vào một Vec, rồi join chúng theo thứ tự.
Xem lời giải
use std::thread;
fn main() { let numbers: Vec<u64> = (1..=1000).collect();
let sums: Vec<u64> = thread::scope(|s| { let mut handles = Vec::new(); for chunk in numbers.chunks(250) { handles.push(s.spawn(move || chunk.iter().sum::<u64>())); } handles.into_iter().map(|h| h.join().unwrap()).collect() });
println!("tổng từng phần: {:?}", sums); println!("tổng cộng: {}", sums.iter().sum::<u64>());}Kết quả:
tổng từng phần: [31375, 93875, 156375, 218875]tổng cộng: 500500move ở đây chỉ chuyển chunk, một slice mượn nhỏ, vào mỗi luồng. Bản thân vector vẫn được mượn chứ không bị move, nên bạn có thể dùng lại numbers sau scope.
2. Mã đơn hàng không trùng lặp
Phần tiêu đề “2. Mã đơn hàng không trùng lặp”Một cửa hàng trực tuyến gán cho mỗi đơn hàng một mã số, bắt đầu từ 1000. Khởi chạy 4 luồng, mỗi luồng lấy 3 mã đơn hàng từ một AtomicU64 dùng chung. Gom cả 12 mã, sắp xếp rồi in ra. Gợi ý: fetch_add(1, Ordering::SeqCst) trả về giá trị trước khi cộng, nên mỗi mã được phát ra đúng một lần.
Xem lời giải
use std::sync::atomic::{AtomicU64, Ordering};use std::thread;
fn main() { let next_order = AtomicU64::new(1000);
let mut numbers: Vec<u64> = thread::scope(|s| { let mut handles = Vec::new(); for _ in 0..4 { handles.push(s.spawn(|| { (0..3) .map(|_| next_order.fetch_add(1, Ordering::SeqCst)) .collect::<Vec<u64>>() })); } handles.into_iter().flat_map(|h| h.join().unwrap()).collect() });
numbers.sort(); println!("{} đơn hàng: {:?}", numbers.len(), numbers);}Kết quả:
12 đơn hàng: [1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011]Luồng nào nhận mã nào thay đổi giữa các lần chạy, nhưng không mã nào bị phát hai lần hay bị bỏ sót. Sắp xếp giúp danh sách in ra lần nào cũng giống nhau.
Tóm tắt
Phần tiêu đề “Tóm tắt”thread::scopeđảm bảo các luồng của nó kết thúc trước khi nó kết thúc, nên chúng có thể mượn dữ liệu cục bộ mà không cầnArchaymove.RwLockcho phép nhiều người đọc hoặc một người ghi. Nó có ích với dữ liệu đọc nhiều; nếu không,Mutexđơn giản hơn.- Atomic (
AtomicUsize,AtomicI64,AtomicBool) xử lý bộ đếm và cờ mà không cần khóa. DùngSeqCstkhi không chắc,Relaxedcho bộ đếm đơn giản. LazyLockvàOnceLocktạo một giá trị toàn cục đúng một lần, an toàn, kể cả khi nhiều luồng cùng hỏi một lúc.- Tránh deadlock: lấy khóa theo cùng một thứ tự, giữ phạm vi khóa ngắn, và drop guard sớm.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Vì sao các luồng khởi chạy bằng s.spawn bên trong thread::scope mượn được biến cục bộ?
2. Phát biểu nào về RwLock là đúng?
3. Bạn cần một bộ đếm lượt xem trang dùng chung cho nhiều luồng. Lựa chọn tốt đơn giản nhất là gì?
4. Luồng A khóa alice rồi bob. Luồng B khóa bob rồi alice. Điều gì có thể xảy ra?
Điểm: 0 / 4
Đọc thêm
Phần tiêu đề “Đọc thêm”- Shared-State Concurrency — The Rust Book (tiếng Anh): chương chính thức về
MutexvàArc, nền tảng mà trang này xây dựng tiếp. std::thread::scope— tài liệu thư viện chuẩn (tiếng Anh): scoped thread hoạt động ra sao, với nhiều ví dụ hơn.std::sync::RwLock— tài liệu thư viện chuẩn (tiếng Anh): guard đọc và ghi, poisoning, và một ghi chú về việc tính công bằng phụ thuộc vào hệ điều hành.std::sync::atomic— tài liệu thư viện chuẩn (tiếng Anh): mọi kiểu atomic và tổng quan ngắn về memory ordering.- Rust Atomics and Locks của Mara Bos (tiếng Anh): một cuốn sách trực tuyến miễn phí giải thích atomic, memory ordering và cách các loại khóa được xây dựng, từng bước một.