Channel memindahkan nilai dari tangan ke tangan, dan perpindahan itulah sumber keamanannya. Namun sebagian data tidak cocok dioper: penghitung kunjungan yang harus dinaikkan setiap kali ada thread selesai, saldo kas yang dikoreksi debit-kredit dari banyak titik, status pekerjaan yang dibaca-ubah oleh seluruh worker. Data seperti itu hidup di satu tempat dan diubah di tempat — perubahan bersama pada memori yang sama. Bahasa lain menyerahkan urusan ini pada kedisiplinan programmer; Rust mengikatnya pada tipe: Mutex<T> menyimpan data di balik kunci, dan satu-satunya jalan menuju data adalah mengunci, memegang guard, lalu melepaskan kunci secara otomatis. Kesalahan lupa membuka kunci — galat klasik C dan C++ — menjadi mustahil ditulis.
Data di Balik Kunci #
Mutex<T> adalah wadah: Mutex::new(250_000) menyimpan angka di dalamnya, persis seperti Box menyimpan nilai. Perbedaannya, Mutex tidak bisa dibuka begitu saja. Metode lock() mencoba mengambil kunci: bila kunci sedang dipegang thread lain, pemanggil tidur sampai kunci bebas; bila bebas, pemanggil mendapat MutexGuard<T> — tanda bukti kepemilikan kunci sekaligus jendela menuju data di dalamnya.
Dua sifat MutexGuard menjadikannya perangkat keamanan. Pertama, guard mengimplementasikan Deref dan DerefMut, sehingga dereference *guard memberi akses baca-tulis ke data — akses itu hanya mungkin selama guard hidup. Kedua, guard melepaskan kunci otomatis saat dijatuhkan, baik karena keluar scope maupun karena drop eksplisit. Pola RAII inilah yang menutup celah lupa-unlock: umur guard dan masa pegang kunci dijahit menjadi satu oleh ownership.
Karena hanya satu guard yang bisa hidup pada satu masa, hanya satu thread yang bisa menyentuh data pada satu masa. Thread lain yang memanggil lock() akan menunggu — penantian inilah yang disebut blocking pada lock.
Mengunci, Mengubah, Melepas #
Mari kita lihat siklus penuhnya tanpa thread tambahan dulu, agar mekanismenya terlihat jelas tanpa perantara penjadwal thread:
use std::sync::Mutex;
fn main() {
let saldo = Mutex::new(250_000);
{
let mut penarikan = saldo.lock().unwrap();
*penarikan -= 50_000;
}
{
let mut setoran = saldo.lock().unwrap();
*setoran += 25_000;
}
println!("Saldo akhir: Rp{}", *saldo.lock().unwrap());
}Output yang Dihasilkan:
Saldo akhir: Rp225000Mari kita bedah kode di atas:
- Baris 4 (
let saldo = Mutex::new(250_000);): saldo kini bertipeMutex<i32>— angka 250000 tidak bisa disentuh tanpa melalui kunci. Perhatikan binding tanpamut:lock()menerima reference biasa (&self) karena mutabilitas data dikelola di dalamMutex, bukan pada variabelnya. - Baris 6-9 (blok pertama): blok
{ ... }sengaja dibuat sebagai pagar umur. Baris 7 mengunci dan mengikat guard kepenarikan— bindingmutdibutuhkan agar*penarikan -= 50_000pada Baris 8 sah, karena dereference mutable atas guard menuntut binding mutable. Pada Baris 9, guard keluar scope dan dijatuhkan — kunci kembali bebas saat itu juga. - Baris 11-14 (blok kedua): siklus yang sama untuk setoran. Antara kedua blok, kunci sempat bebas — thread lain bisa masuk di celah itu.
- Baris 16 (
println!("Saldo akhir: Rp{}", *saldo.lock().unwrap());): pola singkat kunci-baca-buang:lock()menghasilkan guard sementara,*membacanya, dan guard dijatuhkan begitu pernyataan selesai. Hasil akhirnya 225000 sesuai 250000 dikurangi 50000 lalu ditambah 25000.
Setiap blok di atas hanya memegang kunci selama diperlukan. Prinsip “area kritis sependek mungkin” tidak dijalankan oleh library — library hanya menyediakan pagar; kita yang menentukan panjang bloknya.
Mutex Bukan Pointer Transparan #
Box, Rc, dan Arc semuanya smart pointer transparan: dereference * langsung menembus ke data. Mutex menolak ke transparan. Kode berikut mencoba membaca saldo seolah-olah Mutex adalah Box:
use std::sync::Mutex;
fn main() {
let saldo = Mutex::new(250_000);
println!("Saldo saat ini: Rp{}", *saldo);
}Kompilasi gagal dengan pesan asli berikut:
error[E0614]: type `std::sync::Mutex<{integer}>` cannot be dereferenced
--> main.rs:6:38
|
6 | println!("Saldo saat ini: Rp{}", *saldo);
| ^^^^^^ can't be dereferenced
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0614`.Pesan E0614 menegaskan desainnya: tipe Mutex tidak mengimplementasikan dereference sama sekali, sehingga *saldo ditolak. Kunci harus diambil lebih dulu — *saldo.lock().unwrap() — dan penolakan ini disengaja. Transparansi akan memungkinkan akses tanpa antre, yang berarti akses tanpa perlindungan; Mutex memaksa setiap sentuhan data berjalan lewat pintu kunci. Ketimbang memandang Mutex<T> sebagai pointer, pandanglah sebagai brankas: isinya tetap milik kita, tetapi setiap pengambilan menuntut pembukaan yang tercatat.
Satu Worker dengan Mutex #
Bagaimana Mutex bekerja di dalam thread? Contoh berikut memindahkan seluruh brankas ke satu worker: worker mengunci berulang kali untuk menaikkan penghitung, lalu melaporkan hasil akhirnya lewat join:
use std::sync::Mutex;
use std::thread;
fn main() {
let kunjungan = Mutex::new(0);
let pekerja = thread::spawn(move || {
for _ in 0..5 {
let mut jumlah = kunjungan.lock().unwrap();
*jumlah += 1;
}
*kunjungan.lock().unwrap()
});
let hasil = pekerja.join().unwrap();
println!("Jumlah kunjungan tercatat: {}", hasil);
}Output yang Dihasilkan:
Jumlah kunjungan tercatat: 5Mari kita bedah kode di atas:
- Baris 7 (
let pekerja = thread::spawn(move || {): brankasnya sendiri —Mutex<i32>— dipindahkan utuh ke worker lewatmove. Sejak baris ini,maintidak bisa mengunci lagi; worker satu-satunya pihak yang memegang brankas. - Baris 8-11 (
for _ in 0..5 { ... }): lima kali siklus kunci-ubah-lepas. Guardjumlahhidup hanya satu iterasi — kunci bebas di akhir tiap iterasi, sehingga celah bagi thread lain tetap terbuka tiap putaran. - Baris 12 (
*kunjungan.lock().unwrap()): nilai akhir penghitung dibaca lewat satu kunci terakhir, lalui32yang tersalin (tipeCopy) menjadi nilai kembalian thread. - Baris 15 (
let hasil = pekerja.join().unwrap();): hasil 5 diambil lewatjoin— pola pertukaran hasil yang sudah kita kuasai.
Contoh ini jujur menunjukkan batasnya juga: satu brankas hanya bisa pindah ke satu pemilik, jadi worker lain tidak bisa ikut menghitung. Membagikan brankas yang sama ke banyak thread menuntut pemilik bersama — Arc<Mutex<i32>> — kombinasi klasik yang kita bangun di Chapter Arc.
Ringkasan #
Mutex<T> menyimpan data di balik kunci; lock() mengantre dan menyerahkan MutexGuard — satu-satunya jendela menuju data lewat dereference, sekaligus penjamin kunci terbuka otomatis saat guard mati (RAII). Akses langsung tanpa kunci ditolak sejak level tipe lewat E0614, dan racun akibat panic di balik kunci tercermin pada Result dari lock. Mutex milik satu thread sudah cukup untuk melatih pola; berbaginya ke banyak thread menuntut pemilik bersama bernama Arc, yang menjadi pokok Chapter selanjutnya.