Mục lục
- Mục Tiêu Bài Học
- static vs static mut — Khác Biệt Cốt Lõi
- Truy Cập static mut Phải Trong unsafe Block
- Race Condition Khi Đa Luồng + Edition 2024 Deny Lint
- Atomic Types — Counter Thread-Safe Không Cần unsafe
- OnceLock<T> — Lazy Init Một Lần (Stdlib)
- once_cell Crate — Lazy<T> Tiện Lợi Hơn
- Khi Nào Vẫn Dùng static mut
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Phân biệt
staticimmutable vàstatic mut: cả hai sống suốt chương trình, nhưngstatic mutcho phép modify với cái giá là unsafe. - Biết mọi truy cập
static mut(đọc lẫn ghi) bắt buộc nằm trongunsafe { }block vì compiler không thể đảm bảo không có race. - Hiểu vì sao race condition là rủi ro thực sự: nhiều thread đọc/ghi cùng địa chỉ memory không atomic dẫn đến UB.
- Nắm lint
static_mut_refsmặc định deny trong edition 2024: cấm lấy reference (&,&mut) tớistatic mut. - Biết 3 giải pháp modern thay thế:
AtomicU32/AtomicBoolcho counter,OnceLock<T>stdlib cho lazy init,once_cell::sync::Lazycho compute on first access. - Hiểu khi nào vẫn phải dùng
static mut:no_std, embedded firmware không có atomic hardware.
static vs static mut — Khác Biệt Cốt Lõi
Cả static và static mut đều khai báo biến global — sống ở một địa chỉ memory cố định suốt thời gian chương trình chạy, lifetime 'static. Khác biệt nằm ở chỗ có cho phép modify hay không.
// Immutable global — safe
static GREETING: &str = "Xin chào";
static MAX_USERS: usize = 1000;
// Mutable global — unsafe
static mut COUNTER: u32 = 0;
Đọc GREETING hay MAX_USERS từ bất kỳ thread nào đều an toàn — vì giá trị không bao giờ đổi, không có race. Compiler đối xử như một hằng số đặt sẵn trong vùng read-only của binary.
Ngược lại, COUNTER được khai báo mut nên giá trị có thể thay đổi. Đây là nơi vấn đề bắt đầu: nếu thread A đang ghi COUNTER = 5 và thread B đang đọc cùng lúc, B có thể thấy giá trị nửa-cũ-nửa-mới (torn read) trên CPU nào không đảm bảo atomic 32-bit write. Trên một số kiến trúc khác (vd ARM 32-bit ghi vào địa chỉ không align), thậm chí ghi một u64 cũng không atomic. Rust không thể bảo đảm an toàn nên đẩy trách nhiệm sang lập trình viên qua keyword unsafe.
Một quy ước: cả static và static mut đều viết SCREAMING_SNAKE_CASE theo RFC 430. Type annotation bắt buộc — compiler không infer cho static. Giá trị khởi tạo phải là const expression (đánh giá được tại compile time) — không thể gọi function thường, không thể alloc heap trực tiếp.
Truy Cập static mut Phải Trong unsafe Block
Quy tắc cứng: mọi đọc hoặc ghi vào static mut phải nằm trong unsafe { } block — không có ngoại lệ.
static mut COUNTER: u32 = 0;
fn increment() {
unsafe {
COUNTER += 1;
}
}
fn read() -> u32 {
unsafe { COUNTER }
}
fn main() {
increment();
increment();
increment();
println!("counter = {}", read()); // 3
}
Trong chương trình single-thread như trên, code chạy đúng — không có thread khác can thiệp. Nhưng compiler không thể phân biệt single-thread hay multi-thread chỉ từ signature, nên rule áp dụng đồng đều: bất cứ khi nào đụng vào static mut, phải unsafe.
Bỏ unsafe sẽ ra compile error rõ ràng:
error[E0133]: use of mutable static is unsafe and requires unsafe function or block
|
| COUNTER += 1;
| ^^^^^^^^^^^^ use of mutable static
Điều này nhắc lại bài 284: static mut access là một trong 5 unsafe operation chính thức (cùng với deref raw pointer, gọi unsafe fn, impl unsafe trait, access union field). Đánh dấu unsafe { } nghĩa là caller đang ký hợp đồng: "tôi đã verify không có race condition tại thời điểm này".
Lưu ý hợp đồng đó rất khó giữ trong codebase có thread. Đó là động cơ chính cho phần tiếp theo.
Race Condition Khi Đa Luồng + Edition 2024 Deny Lint
Ví dụ kinh điển về race condition — 10 thread cùng increment một static mut COUNTER:
use std::thread;
static mut COUNTER: u32 = 0;
fn main() {
let handles: Vec<_> = (0..10)
.map(|_| thread::spawn(|| {
for _ in 0..1000 {
unsafe { COUNTER += 1; } // race
}
}))
.collect();
for h in handles { h.join().unwrap(); }
unsafe { println!("counter = {}", COUNTER); }
// Mong đợi 10_000 — thực tế thường nhỏ hơn (vd 7234, 8891...)
}
Vì sao thiếu? COUNTER += 1 thực ra là ba bước: đọc giá trị hiện tại → cộng 1 → ghi lại. Nếu thread A đọc 100, thread B cũng đọc 100, A ghi 101, B ghi 101 — mất một lượt tăng. Đó là data race, theo định nghĩa của Rust là undefined behavior: compiler có thể optimize dựa giả định không có data race, output không xác định.
Edition 2024 bổ sung một lớp bảo vệ nữa: lint static_mut_refs mặc định deny — cấm hẳn việc lấy reference (& hoặc &mut) tới static mut:
static mut DATA: [u8; 4] = [0, 0, 0, 0];
fn main() {
unsafe {
let r = &DATA; // deny trong edition 2024
println!("{:?}", r);
}
}
error: creating a shared reference to mutable static
--> src/main.rs:5:18
|
5 | let r = &DATA;
| ^^^^^ shared reference to mutable static
|
= note: this will be a hard error in the 2024 edition
= note: `#[deny(static_mut_refs)]` on by default
Lý do: reference đến static mut sống "vô thời hạn" trong khi giá trị có thể bị thread khác sửa cùng lúc — vi phạm aliasing model của Rust ngay cả trong unsafe code. Edition 2024 ép developer chuyển sang dùng raw pointer (&raw const DATA, &raw mut DATA) hoặc — tốt hơn — bỏ luôn static mut.
Atomic Types — Counter Thread-Safe Không Cần unsafe
Với counter primitive (số đếm, flag bool, status code), giải pháp chuẩn là atomic types trong std::sync::atomic: AtomicU32, AtomicI64, AtomicBool, AtomicUsize...
use std::sync::atomic::{AtomicU32, Ordering};
use std::thread;
static COUNTER: AtomicU32 = AtomicU32::new(0);
fn main() {
let handles: Vec<_> = (0..10)
.map(|_| thread::spawn(|| {
for _ in 0..1000 {
COUNTER.fetch_add(1, Ordering::Relaxed);
}
}))
.collect();
for h in handles { h.join().unwrap(); }
println!("counter = {}", COUNTER.load(Ordering::Relaxed));
// Luôn 10_000 — chính xác
}
Khác biệt quan trọng:
- Khai báo
static COUNTER: AtomicU32— không cómut. Atomic types có interior mutability: vẫn thay đổi được giá trị bên trong qua method, không cần&mut. - Mọi operation (
load,store,fetch_add,compare_exchange) là safe fn — không cầnunsafe { }block. - Hardware atomic instruction (vd
LOCK XADDtrên x86) đảm bảo read-modify-write hoàn thành nguyên tử — không race.
Tham số Ordering điều khiển memory ordering — Relaxed đủ cho counter đơn giản; cần ordering mạnh hơn (Acquire/Release/SeqCst) khi cần đồng bộ với data khác. Chi tiết memory ordering sẽ học ở group Concurrency.
Quy tắc thực dụng: bất cứ khi nào bạn nghĩ tới static mut COUNTER: u32, hãy dùng AtomicU32 thay thế. Không có lý do nào để dùng static mut cho integer/bool trong code hosted environment.
OnceLock<T> — Lazy Init Một Lần (Stdlib)
Atomic chỉ làm việc với type primitive. Khi cần global type phức tạp — HashMap, Vec, struct config — và muốn init lười (lazy) một lần khi truy cập đầu tiên, dùng std::sync::OnceLock<T> (stable từ Rust 1.70).
use std::collections::HashMap;
use std::sync::OnceLock;
static CONFIG: OnceLock<HashMap<String, String>> = OnceLock::new();
fn config() -> &'static HashMap<String, String> {
CONFIG.get_or_init(|| {
let mut m = HashMap::new();
m.insert("host".into(), "localhost".into());
m.insert("port".into(), "8080".into());
m
})
}
fn main() {
println!("host = {}", config().get("host").unwrap());
println!("port = {}", config().get("port").unwrap());
// get_or_init chỉ chạy closure đúng 1 lần, dù gọi từ nhiều thread
}
Đặc điểm OnceLock:
- Init lười: closure chỉ chạy lần đầu gọi
get_or_init. Nếu app không bao giờ truy cậpCONFIG, không có cost init. - Thread-safe: nhiều thread cùng gọi
get_or_init, chỉ một thread thực sự chạy closure, các thread còn lại đợi và nhận cùng reference. - Trả về
&'static T— borrow vô thời hạn, an toàn vì giá trị không bị overwrite (chỉ set một lần). - Không cần
unsafeở bất kỳ chỗ nào.
So với static mut HASHMAP: Option<HashMap<_, _>> = None; — pattern cũ trong Rust 1.x — OnceLock ngắn hơn, an toàn hơn, và là lựa chọn chuẩn hiện nay trong stdlib.
once_cell Crate — Lazy<T> Tiện Lợi Hơn
Crate once_cell ra đời trước OnceLock stdlib và cung cấp thêm tiện ích Lazy<T> — wrapper kết hợp OnceLock + init closure ngay tại điểm khai báo:
# Cargo.toml
[dependencies]
once_cell = "1.20"
use once_cell::sync::Lazy;
use std::collections::HashMap;
static CONFIG: Lazy<HashMap<&str, &str>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("host", "localhost");
m.insert("port", "8080");
m
});
fn main() {
println!("host = {}", CONFIG["host"]);
println!("port = {}", CONFIG["port"]);
// Truy cập trực tiếp như giá trị bình thường nhờ Deref
}
Ưu điểm so với OnceLock: đóng gói init closure ngay tại site khai báo, không cần helper function gọi get_or_init. Lazy<T> impl Deref<Target = T> nên gọi method và index như giá trị thường.
Stdlib có wrapper tương tự — std::sync::LazyLock — stable từ Rust 1.80. Cú pháp gần như giống hệt once_cell::sync::Lazy, dùng được mà không cần dependency:
use std::sync::LazyLock;
use std::collections::HashMap;
static CONFIG: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
HashMap::from([("host", "localhost"), ("port", "8080")])
});
Khuyến nghị 2026: project mới dùng LazyLock stdlib trừ khi cần feature thêm của once_cell (vd OnceCell non-thread-safe cho single-thread context, hoặc support no_std với feature flag riêng).
Khi Nào Vẫn Dùng static mut
static mut chưa bị deprecate hoàn toàn vì còn lý do tồn tại trong một nhóm context cụ thể:
- Embedded firmware /
no_std: nhiều microcontroller (ARM Cortex-M0, một số RISC-V đơn giản) không có atomic instruction cho integer ≥ 32 bit, hoặc atomic chỉ có một subset hạn chế. Cratecore::sync::atomictrên target này thiếu nhiều type — buộc dùngstatic mutkèm critical section (disable interrupt) để bảo vệ truy cập. - FFI với C library: khi link với C global state (vd
extern "C" static mut errno) — C semantic vốn là global mutable, Rust phải bridge quastatic mut. - Interrupt handler trong kernel/driver: code chạy trong interrupt context không thể block hoặc allocate, đôi khi đơn giản nhất là
static mut+ manual critical section.
Ngay cả trong các context trên, modern crate như portable-atomic, critical-section, embassy-sync đang phổ biến hoá pattern an toàn hơn — cộng đồng embedded Rust 2026 cũng đẩy mạnh việc tránh static mut trực tiếp khi có thể.
Tổng Kết
statickhai báo biến global lifetime'static;static mutthêm khả năng modify với cái giá là unsafe.- Mọi đọc/ghi
static mutbắt buộc trongunsafe { }block — compiler không thể đảm bảo không có race. - Multi-thread đọc/ghi
static mutkhông atomic là data race → undefined behavior, output không xác định. - Edition 2024: lint
static_mut_refsmặc định deny — cấm lấy reference (&,&mut) tớistatic mut, buộc dùng raw pointer hoặc giải pháp khác. - Counter primitive (số đếm, flag): dùng
AtomicU32,AtomicBool,AtomicUsize... — thread-safe, không cầnunsafe, có hardware atomic instruction. - Global type phức tạp (
HashMap, struct config) cần init lazy:std::sync::OnceLock<T>hoặcstd::sync::LazyLock<T>(Rust 1.80+) — stable trong stdlib. Crateonce_cell::sync::Lazylà alternative phổ biến với cú pháp ngắn gọn. static mutchỉ còn lý do dùng trongno_std/embedded khi không có atomic hardware, FFI với C global, hoặc interrupt handler — kèm critical section.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Vì sao Rust bắt buộc
unsafeđể truy cậpstatic mutkể cả trong chương trình single-thread, không có thread spawn? Quy tắc có quá nghiêm khắc không? - Đoạn code sau chạy trên Rust 2024 edition. Output có ổn định không? Có UB không? Vì sao?
static mut N: u64 = 0; thread::spawn(|| unsafe { N += 1 });(spawn 100 thread). - Khi nào nên chọn
AtomicU32thay vìOnceLock<u32>? Cho ví dụ use case rõ ràng cho từng cái. - Edition 2024 đẩy lint
static_mut_refsthành deny. Đoạnunsafe { let r = &mut DATA; *r = 5; }vớistatic mut DATA: u32 = 0có compile không? Nếu không, viết lại bằng API thread-safe modern. - So sánh
std::sync::OnceLockvớistd::sync::LazyLock: hai cái khác nhau ở điểm nào, khi nào dùng cái nào? - Một firmware ARM Cortex-M0 không có atomic 32-bit, cần counter tăng từ interrupt handler. Bạn vẫn phải dùng
static mut. Mô tả pattern an toàn (gợi ý: critical section).
Đáp án
- Compiler không thể phân biệt single-thread và multi-thread chỉ từ signature của hàm — function dùng
static muthôm nay là single-thread, mai có thể bị gọi từthread::spawn. Yêu cầuunsafeđồng đều buộc developer ý thức rủi ro và xem xét lại trước khi cho phép concurrent access. Quy tắc không quá nghiêm — vì cost của race condition là UB silent rất khó debug, "nghiêm khắc tại compile time" rẻ hơn nhiều so với "debug data race tại production". - Không ổn định và có UB.
N += 1= đọc + cộng + ghi, ba bước không nguyên tử. 100 thread cùng làm sẽ mất một số lượt tăng (lost update) — output có thể là bất kỳ giá trị nào từ ~50 đến 100. Theo định nghĩa của Rust, data race trênstatic mutlà undefined behavior — compiler có thể optimize dựa giả định không có race, kết quả không xác định và không ai "đúng". Fix: thay bằngstatic N: AtomicU64 = AtomicU64::new(0);vàN.fetch_add(1, Ordering::Relaxed). AtomicU32cho giá trị thay đổi liên tục trong runtime (counter request, gauge metric, flag bật/tắt).OnceLock<u32>cho giá trị set một lần duy nhất và sau đó chỉ đọc — vd config value đọc từ env var khi truy cập đầu tiên. Ví dụAtomicU32: đếm số request HTTP đang xử lý. Ví dụOnceLock<u32>: port server đọc từstd::env::var("PORT")parse u32, sau đó cố định suốt đời app.- Không compile. Edition 2024 deny
static_mut_refs, lấy&mut DATAlà hard error. Có hai cách viết lại: (a) dùng raw pointer —unsafe { *(&raw mut DATA) = 5; }vẫn unsafe nhưng tránh lint; (b) khuyến nghị — đổi sangstatic DATA: AtomicU32 = AtomicU32::new(0); DATA.store(5, Ordering::Relaxed);không unsafe, thread-safe. Cách (b) luôn nên ưu tiên trừ khi có lý do hardware đặc biệt. OnceLock<T>là cell rỗng, gọiget_or_init(closure)mỗi lần muốn lấy giá trị — init closure ở callsite, linh hoạt (vd lấy config khác nhau theo context).LazyLock<T>đóng gói init closure ngay tại điểm khai báostatic, truy cập như giá trị thường nhờDeref— gọn hơn nhưng init closure cố định. DùngLazyLockkhi global chỉ cần một cách init duy nhất (vd CONFIG mặc định); dùngOnceLockkhi muốn quyết định cách init tại runtime hoặc init phụ thuộc context.- Pattern:
static mut COUNTER: u32 = 0;kèm critical-section wrapper. Dùng cratecritical-section:critical_section::with(|_cs| unsafe { COUNTER += 1; })— closure chạy với interrupt disable trên Cortex-M (CPSID i / CPSIE i), đảm bảo nguyên tử ở mức một core. Interrupt handler đọcCOUNTERcũng wrap trongcritical_section::withđể không bị nested interrupt làm hỏng. Trên multi-core embedded chip thì critical section chưa đủ — cần spinlock hoặc đổi sang chip có atomic. Crateportable-atomiccũng cung cấpAtomicU32emulated dùng critical section nội bộ, là alternative cleaner hơn so vớistatic mutthô.
Bài Tiếp Theo
Bài 287: FFI — extern "C" Overview — bài cuối của Group 35 Unsafe Rust. Tìm hiểu cách gọi C library từ Rust qua extern "C" { ... } block, expose Rust API ra C bằng #[no_mangle] extern "C" fn, dùng #[repr(C)] cho struct interop ABI ổn định, và preview tool bindgen để auto-generate FFI binding từ header C.
