Danh sách bài viết

Bài 286: static mut — Global Mutable

Bài 286 của series Rust Cơ Bản — static mut là cách khai báo global mutable variable trong Rust. Cú pháp đơn giản nhưng hậu quả không đơn giản: vì nhiều thread có thể đọc/ghi đồng thời nên mọi truy cập đều bắt buộc nằm trong unsafe { } block, và edition 2024 còn đẩy lint static_mut_refs thành deny mặc định để cấm hẳn việc lấy reference. Modern Rust khuyến nghị thay thế bằng AtomicU32/AtomicBool cho counter primitive, OnceLock<T> cho lazy init một lần (stdlib), hoặc crate once_cell với Lazy<T>. static mut chỉ còn lý do tồn tại trong no_std và embedded firmware nơi không có atomic hardware hỗ trợ.

10/06/2026
9 phút đọc
1 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Phân biệt static immutable và static mut: cả hai sống suốt chương trình, nhưng static mut cho 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 trong unsafe { } 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_refs mặc định deny trong edition 2024: cấm lấy reference (&, &mut) tới static mut.
  • Biết 3 giải pháp modern thay thế: AtomicU32/AtomicBool cho counter, OnceLock<T> stdlib cho lazy init, once_cell::sync::Lazy cho 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.
2

static vs static mut — Khác Biệt Cốt Lõi

Cả staticstatic 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ả staticstatic 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.

3

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.

4

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.

5

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: AtomicU32khô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ần unsafe { } block.
  • Hardware atomic instruction (vd LOCK XADD trê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.

6

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ập CONFIG, 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.

7

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).

8

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ế. Crate core::sync::atomic trên target này thiếu nhiều type — buộc dùng static mut kè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 qua static 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ể.

9

Tổng Kết

  • static khai báo biến global lifetime 'static; static mut thêm khả năng modify với cái giá là unsafe.
  • Mọi đọc/ghi static mut bắt buộc trong unsafe { } block — compiler không thể đảm bảo không có race.
  • Multi-thread đọc/ghi static mut không atomic là data race → undefined behavior, output không xác định.
  • Edition 2024: lint static_mut_refs mặc định deny — cấm lấy reference (&, &mut) tới static 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ần unsafe, có hardware atomic instruction.
  • Global type phức tạp (HashMap, struct config) cần init lazy: std::sync::OnceLock<T> hoặc std::sync::LazyLock<T> (Rust 1.80+) — stable trong stdlib. Crate once_cell::sync::Lazy là alternative phổ biến với cú pháp ngắn gọn.
  • static mut chỉ còn lý do dùng trong no_std/embedded khi không có atomic hardware, FFI với C global, hoặc interrupt handler — kèm critical section.
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Vì sao Rust bắt buộc unsafe để truy cập static mut kể cả trong chương trình single-thread, không có thread spawn? Quy tắc có quá nghiêm khắc không?
  2. Đ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).
  3. Khi nào nên chọn AtomicU32 thay vì OnceLock<u32>? Cho ví dụ use case rõ ràng cho từng cái.
  4. Edition 2024 đẩy lint static_mut_refs thành deny. Đoạn unsafe { let r = &mut DATA; *r = 5; } với static mut DATA: u32 = 0 có compile không? Nếu không, viết lại bằng API thread-safe modern.
  5. So sánh std::sync::OnceLock với std::sync::LazyLock: hai cái khác nhau ở điểm nào, khi nào dùng cái nào?
  6. 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
  1. Compiler không thể phân biệt single-thread và multi-thread chỉ từ signature của hàm — function dùng static mut hôm nay là single-thread, mai có thể bị gọi từ thread::spawn. Yêu cầu unsafe đồ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".
  2. 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ên static mut là 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ằng static N: AtomicU64 = AtomicU64::new(0);N.fetch_add(1, Ordering::Relaxed).
  3. AtomicU32 cho 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.
  4. Không compile. Edition 2024 deny static_mut_refs, lấy &mut DATA là 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 sang static 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.
  5. OnceLock<T> là cell rỗng, gọi get_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áo static, truy cập như giá trị thường nhờ Deref — gọn hơn nhưng init closure cố định. Dùng LazyLock khi global chỉ cần một cách init duy nhất (vd CONFIG mặc định); dùng OnceLock khi muốn quyết định cách init tại runtime hoặc init phụ thuộc context.
  6. Pattern: static mut COUNTER: u32 = 0; kèm critical-section wrapper. Dùng crate critical-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 đọc COUNTER cũng wrap trong critical_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. Crate portable-atomic cũng cung cấp AtomicU32 emulated dùng critical section nội bộ, là alternative cleaner hơn so với static mut thô.
11

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.