Kadang sebuah nilai memang perlu hidup sepanjang program dan terlihat dari banyak tempat: penghitung transaksi harian, konfigurasi yang diatur sekali saat start, atau status mesin yang dipantau berkala. Rust memiliki static untuk kebutuhan itu — tetapi begitu kata mut ditambahkan, statusnya berubah drastis: variabel global yang dapat diubah adalah pintu menuju race data karena tidak ada pemilik tunggal yang mengatur antreannya. Aturan &'static yang kita kenal di Section Koncurrency memang mengizinkan pembacaan bersama lintas thread, namun penulisan bersama tidak pernah aman tanpa penguncian. Karena itu Rust mengurung setiap akses static mut di balik unsafe. Chapter ini mempraktikkan pola aksesnya di program satu thread, membaca pesan error penegaknya, lalu membahas pintu keluar paling berisiko: unsafe impl Send yang menjadikan kita pemberi jaminan Send secara manual.
static mut dan Aturan Aksesnya #
Deklarasi static mut mirip static biasa dengan satu kata kunci tambahan yang mengubah segalanya. Nilainya tersimpan di bagian data binary program, umurnya 'static secara otomatis, dan namanya dapat diakses dari fungsi mana pun di crate yang sama. Hukumannya: setiap operasi baca maupun tulis — bahkan sekadar += 1 — wajib berada di dalam blok atau fungsi unsafe. Mari kita lihat pola lengkapnya pada penghitung pesanan warung:
static mut TOTAL_PESANAN: u32 = 0;
fn catat_pesanan() {
unsafe {
TOTAL_PESANAN += 1;
}
}
fn main() {
catat_pesanan();
catat_pesanan();
catat_pesanan();
let total = unsafe { TOTAL_PESANAN };
println!("Total pesanan hari ini: {}", total);
}Output yang Dihasilkan:
Total pesanan hari ini: 3Mari kita bedah kode di atas baris per baris:
- Baris 1 (
static mut TOTAL_PESANAN: u32 = 0;): variabel global mutable bertipeu32lahir dengan nilai awal 0 di memori data program. Berbeda darilet, tipe padastatictidak boleh diinferensikan dan harus ditulis eksplisit. - Baris 4 (
unsafe {): pagar dibuka di dalamcatat_pesanankarena baris berikutnya menyentuh global mutable — fungsi pembantu inilah satu-satunya pintu penulisan, pola yang jauh lebih terkontrol daripada menulisunsafedi banyak titik. - Baris 5 (
TOTAL_PESANAN += 1;): operasi baca-tulis langsung terhadap alamat global. Karena program ini satu thread, tidak ada pesaing dan hasilnya pasti terhitung tiga kali. - Baris 14 (
let total = unsafe { TOTAL_PESANAN };): pembacaan nilai dilakukan lewat blokunsafeberbentuk ekspresi — nilainya disalin ke variabel lokaltotal, sehingga pencetakan di luar blok sudah memakai salinan aman. - Baris 15 (
println!("Total pesanan hari ini: {}", total);): pencetakan memakai variabel lokal biasa; tidak ada operasiunsafeyang tersisa di luar pagar.
Error Asli: Akses Tanpa unsafe #
Penegakan aturannya terlihat begitu pagar dilupakan. Program berikut mengakses TOTAL_PESANAN seperti variabel biasa:
static mut TOTAL_PESANAN: u32 = 0;
fn main() {
TOTAL_PESANAN += 1;
println!("Total pesanan: {}", TOTAL_PESANAN);
}Kompilasi gagal dengan dua error E0133 sekaligus — satu untuk penulisan, satu untuk pembacaan:
error[E0133]: use of mutable static is unsafe and requires unsafe function or block
--> main.rs:4:5
|
4 | TOTAL_PESANAN += 1;
| ^^^^^^^^^^^^^ use of mutable static
|
= note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
error[E0133]: use of mutable static is unsafe and requires unsafe function or block
--> main.rs:5:35
|
5 | println!("Total pesanan: {}", TOTAL_PESANAN);
| ^^^^^^^^^^^^^ use of mutable static
|
= note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0133`.Pesan E0133 memakai rumusan “use of mutable static” — berbeda dari “dereference of raw pointer” yang kita temui di Chapter sebelumnya, tetapi alasan di catatannya sama tuanya: mutable statics can be mutated by multiple threads. Kompilator tidak bisa membuktikan bahwa hanya satu thread menyentuh variabel ini pada satu waktu, karena memang tidak ada pembatas kepemilikan seperti pada let — global bisa dijangkau siapa saja. Dua error muncul karena kedua baris sama-sama melanggar; pagar unsafe harus melingkupi keduanya.
Bayangan Race Data di Multi-Thread #
Contoh kita aman murni karena main berjalan sendirian. Bayangkan catat_pesanan dipanggil dari dua thread hasil spawn: thread A membaca TOTAL_PESANAN bernilai 3, thread B juga membaca 3, keduanya menjumlahkan 1 secara terpisah, lalu keduanya menulis 4. Dua pesanan masuk, penghitung hanya naik satu — lost update yang khas pada data tanpa penguncian. Kenyataannya lebih buruk: kompiler berhak mengoptimalkan pembacaan berulang (nilai global “tidak berubah” dari sudut pandang satu thread tanpa sinkronisasi), menyusun ulang urutan baca-tulis, dan membuat asumsi aliasing. Itulah mengapa catatan E0133 menyebut aliasing violations or data races will cause undefined behavior — bukan sekadar “angka bisa salah”, melainkan “perilaku program tidak lagi dijamin apa pun”.
Praktik yang sehat hampir selalu memilih salah satu dari tiga alternatif aman: static TOTAL: AtomicU32 untuk penghitung sederhana (operasi fetch_add bebas race tanpa unsafe), static TOTAL: Mutex<u32> untuk data kompleks, atau thread_local! untuk global yang memang cukup per-thread. static mut layak dipertimbangkan hanya pada wilayah embedded atau integrasi pustaka C yang benar-benar single-threaded — dan sekalipun demikian, membungkusnya dalam fungsi-fungsi pengakses tunggal seperti catat_pesanan di atas adalah bentuk paling minim risikonya.
unsafe impl Send: Menjadi Pemberi Jaminan #
Dari semua tempat unsafe bersembunyi di crate pihak lain, satu pola muncul berulang: tipe pembungkus yang memuat raw pointer. Tabel di Chapter Raw Pointer menunjukkan *mut T berstatus !Send, sehingga tipe apa pun yang memuatnya otomatis ditolak thread::spawn. Program berikut membuktikan penolakannya:
use std::thread;
struct BufferEksternal {
ptr: *mut u8,
kapasitas: usize,
}
fn main() {
let buffer = BufferEksternal {
ptr: std::ptr::null_mut(),
kapasitas: 1024,
};
let pegangan = thread::spawn(move || {
println!("Kapasitas: {}, pointer kosong: {}",
buffer.kapasitas, buffer.ptr.is_null());
});
pegangan.join().unwrap();
}Kompilasi gagal dengan pesan asli berikut:
error[E0277]: `*mut u8` cannot be sent between threads safely
--> main.rs:14:34
|
14 | let pegangan = thread::spawn(move || {
| ------------- ^------
| | |
| ____________________|_____________within this `{[email protected]:14:34: 14:41}`
| | |
| | required by a bound introduced by this call
15 | | println!("Kapasitas: {}, pointer kosong: {}",
16 | | buffer.kapasitas, buffer.ptr.is_null());
17 | | });
| |_____^ `*mut u8` cannot be sent between threads safely
|
= help: within `{[email protected]:14:34: 14:41}`, the trait `Send` is not implemented for `*mut u8`
note: required because it's used within this closure
--> main.rs:14:34
|
14 | let pegangan = thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
--> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/std/src/thread/functions.rs:125:0
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.Penolakan ini masuk akal: *mut u8 tidak membawa informasi kepemilikan, dan kompiler tidak mau menebak. Namun ada kalanya kita tahu lebih banyak dari kompiler — misalnya buffer C yang hanya berpindah kepemilikan antar thread dan tidak pernah diakses bersamaan. Untuk itulah kemampuan keempat dari daftar Chapter Kapan Unsafe: mengimplementasikan unsafe trait secara manual. Trait Send adalah marker trait tanpa metode, jadi unsafe impl Send tidak mengubah perilaku apa pun — implementasi ini semata pernyataan tanda tangan kita bahwa jaminan aman lintas thread terpenuhi:
use std::thread;
struct BufferEksternal {
ptr: *mut u8,
kapasitas: usize,
}
// Jaminan programmer: kepemilikan buffer hanya berpindah
// antar thread dan tidak pernah dipakai bersamaan.
unsafe impl Send for BufferEksternal {}
fn main() {
let buffer = BufferEksternal {
ptr: std::ptr::null_mut(),
kapasitas: 1024,
};
let pegangan = thread::spawn(move || {
let buf = buffer;
println!("Kapasitas: {}, pointer kosong: {}",
buf.kapasitas, buf.ptr.is_null());
});
pegangan.join().unwrap();
println!("Thread selesai");
}Output yang Dihasilkan:
Kapasitas: 1024, pointer kosong: true
Thread selesaiMari kita bedah kode di atas baris per baris:
- Baris 10 (
unsafe impl Send for BufferEksternal {}): implementasi marker trait ditandaiunsafekarena compiler tidak dapat memverifikasi klaimnya — sama seperti penanda pada fungsi, penanda di sini memindahkan tanggung jawab soundness ke kita. Penolakan E0277 di program sebelumnya kini hilang seketika. - Baris 18 (
let pegangan = thread::spawn(move || {): closuremovemengambil kepemilikan seluruh struct. DenganSendyang kini berlaku untukBufferEksternal,spawnmeloloskan pemindahan thread ini tanpa protes. - Baris 19 (
let buf = buffer;): struct diikat ke variabel lokal di dalam thread — closure memindahkan seluruh nilai, bukan field per field. Pointer dan kapasitas berpindah sebagai satu kesatuan kepemilikan. - Baris 20–21 (
println!("Kapasitas: {}, ...", buf.kapasitas, buf.ptr.is_null());): thread hanya membaca dua field tanpa menulis memori yang ditunjukptr, sehingga jaminan “tidak pernah dipakai bersamaan” tidak pernah teruji — program benar karena invariant yang dijanjikan paling sederhana (pointer bahkan null) tetap terjaga.
Ringkasan #
static mut menyimpan global mutable berumur 'static yang setiap aksesnya — baca maupun tulis — wajib berada dalam unsafe dan dilindungi error E0133 “use of mutable static”. Di program single-threaded polanya sederhana: fungsi pengakses tunggal menulis, blok ekspresi unsafe menyalin nilai untuk dibaca. Di dunia multi-thread, race data pada static mut adalah undefined behavior, sehingga AtomicU32, Mutex<T>, atau thread_local! menjadi pilihan yang jauh lebih sehat. Kemampuan unsafe impl Send memungkinkan tipe pembungkus raw pointer menyeberangi thread dengan jaminan manual — kontrak yang tidak diverifikasi siapa pun kecuali kita sendiri. Seluruh pengalaman ini bermuara pada satu pertanyaan berikutnya: bagaimana membungkus risiko-risiko tersebut menjadi API yang pemanggilnya tidak perlu tahu? Jawabannya adalah safe abstraction, topik chapter penutup bagian implementasi.