Chapter sebelumnya menutup dengan satu kebutuhan yang belum terpenuhi: Mutex adalah brankas, tetapi brankas tetap satu benda milik satu pemilik — kepemilikan brankas harus bisa dibagi agar banyak thread sama-sama bisa mengunci. Section Smart Pointers pernah menyelesaikan kepemilikan bersama untuk dunia satu thread lewat Rc<T>; penasib lintas thread bernama Arc<T> kini mengambil panggung. Nama itu singkatan dari atomically reference counted, dan seluruh perbedaannya dengan Rc terletak pada satu kata — atomically — yang menjadikan penghitung pemilik aman diinjak banyak thread sekaligus. Chapter ini membedah penghitung tersebut, lalu menyusun kombinasi paling terkenal di Rust concurrency: Arc<Mutex<T>>.
Reference Count yang Atomik #
Mekanisme dasarnya sama dengan Rc. Arc::new(nilai) mengalokasikan satu petak heap berisi nilai tersebut dan sebuah penghitung pemilik yang dimulai dari 1. Setiap Arc yang beredar hanyalah penunjuk ramping menuju petak itu. Arc::clone(&arc) tidak menyalin data — operasi ini hanya menaikkan penghitung satu angka lalu mengembalikan penunjuk baru; Drop menurunkan satu angka, dan nilai di dalam petak benar-benar dijatuhkan ketika penghitung mencapai nol.
Perbedaannya lahir dari cara angka dinaikkan. Rc menaikkan penghitung dengan operasi penjumlahan biasa — langkah yang bisa terpotong di tengah oleh thread lain pada dunia paralel: dua thread yang meng-clone nyaris bersamaan bisa sama-sama membaca angka 1, sama-sama menulis 2, dan penghitung pun kehilangan satu pemilik. Arc menaikkan penghitung lewat instruksi atomic — penjumlahan yang dijamin kernel dan CPU menjadikannya tidak terputus dan tidak saling menginjak, berapa pun thread yang berusaha bersamaan. Itulah seluruh harga yang dibayar Arc: setiap clone dan drop sedikit lebih mahal daripada Rc, sebagai bayaran kepastian.
Sifat kedua yang diwariskan aturan ownership: isi Arc tidak bisa diubah. Arc membagikan akses baca — Deref miliknya memberi reference biasa — karena reference mutable terhadap data yang dilihat banyak pemilik melanggar aturan aliasing dari Section Ownership dan Memory. Data bersama yang perlu diubah harus dikunci; pembagian pemilik dan pengurusan mutabilitas adalah dua pekerjaan berbeda.
Mengintip Penghitung Pemilik #
Arc::strong_count memperlihatkan angka penghitung pada suatu saat — cara paling jujur untuk melihat mekanisme ini bekerja:
use std::sync::Arc;
fn main() {
let laporan = Arc::new(String::from("Laporan Tahunan"));
println!("Setelah dibuat : {}", Arc::strong_count(&laporan));
let salinan = Arc::clone(&laporan);
println!("Setelah satu clone : {}", Arc::strong_count(&laporan));
drop(salinan);
println!("Setelah salinan drop : {}", Arc::strong_count(&laporan));
}Output yang Dihasilkan:
Setelah dibuat : 1
Setelah satu clone : 2
Setelah salinan drop : 1Mari kita bedah kode di atas:
- Baris 4 (
let laporan = Arc::new(String::from("Laporan Tahunan"));): petak heap tercipta berisi teks laporan dan penghitung bernilai 1.laporanadalah penunjuk pertama menuju petak tersebut. - Baris 8 (
let salinan = Arc::clone(&laporan);): teks “Laporan Tahunan” tidak tersalin — yang terjadi hanyalah penghitung naik menjadi 2 dansalinankini menunjuk petak yang sama. KonvensiArc::clone(&...)dipilih justru untuk membuat penambahan pemilik mudah terlihat di kode. - Baris 11 (
drop(salinan);): penghitung turun menjadi 1. Nilai di petak tetap hidup karena masih ada pemilik — hanya penunjuknya yang berkurang. - Baris 12: setelah
mainberakhir,laporansebagai pemilik terakhir ikut dijatuhkan, penghitung mencapai 0, dan barulahStringdi heap di-drop.
Arc dan Mutex: Kombinasi Klasik #
Dua perangkat ini saling melengkapi dengan pembagian kerja yang tegas: Arc membagikan kepemilikan brankas ke banyak thread, Mutex menjaga agar isi brankas hanya disentuh satu thread pada satu masa. Contoh klasiknya adalah penghitung bersama — empat kasir masing-masing mencatat pengunjung yang mereka layani, dan angka akhirnya harus tunggal:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let pengunjung = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..4 {
let pengunjung_kasir = Arc::clone(&pengunjung);
handles.push(thread::spawn(move || {
for _ in 0..10 {
let mut jumlah = pengunjung_kasir.lock().unwrap();
*jumlah += 1;
}
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Total pengunjung hari ini: {}", *pengunjung.lock().unwrap());
}Output yang Dihasilkan:
Total pengunjung hari ini: 40Mari kita bedah kode di atas:
- Baris 5 (
let pengunjung = Arc::new(Mutex::new(0));): struktur bertingkat —Mutex<i32>di dalamArc. Empat penunjuk akan menuju brankas yang sama; isi brankas hanya bisa diubah lewat kunci. - Baris 9 (
let pengunjung_kasir = Arc::clone(&pengunjung);): setiap iterasi menaikkan penghitung atomik dan menyiapkan satu penunjuk untuk kasir berikutnya. Empat pemilik berdiri sebelum thread pertama pun meluncur. - Baris 10 (
handles.push(thread::spawn(move || {): closuremovemengambil penunjuk kasir — penunjuk berukuran satu word, bukan salinan data. Perhatikan bahwamovedi sini memindahkanArc-nya, bukan isinya. - Baris 11-14 (
for _ in 0..10 { ... }): tiap kasir sepuluh kali mengunci, menaikkan angka satu, lalu melepas kunci di akhir iterasi. Empat puluh kenaikan tersebar di empat thread, danMutexmemastikan tidak ada dua kenaikan yang menimpa satu sama lain — inilah race condition yang dicegah. - Baris 18-20 (
for handle in handles { ... }):mainmenunggu keempat kasir rampung. Setelahjointerakhir, seluruh pekerjaan pasti sudah tercatat. - Baris 22 (
println!(...)):mainmasih memegang penunjukpengunjung, jadi cukup mengunci sekali untuk membaca angka final: 4 kasir kali 10 kunjungan = 40. Keluaran program ini identik pada setiap eksekusi berkat pagarjoin.
Tanpa Mutex, keempat thread hanya bisa membaca penghitung — dan tanpa Arc, brankasnya hanya bisa pindah ke satu kasir. Kombinasi keduanya adalah pola baku Rust untuk shared mutable state yang aman.
Kapan Arc, Kapan Rc #
Pilihan antar keduanya bisa diringkas menjadi satu pertanyaan: apakah data bisa dipegang lebih dari satu thread? Bila ya, Arc satu-satunya pilihan yang lolos kompilasi. Bila seluruh pemilik pasti berada di satu thread — daftar tetangga pada struktur graf satu thread, konfigurasi yang dibagikan antar modul — Rc lebih tepat karena penghitungnya murah tanpa instruksi atomic. Biaya Arc::clone sendiri tetap rendah untuk ukuran operasi thread: satu penambahan atomic, tanpa menyalin data, tanpa alokasi baru. Kecuali di loop super ketat, biaya itu nyaris tak terasa dibanding biaya spawn thread itu sendiri.
Ringkasan #
Arc<T> membagikan kepemilikan satu data heap ke banyak pemilik dengan penghitung yang dinaikkan dan diturunkan secara atomic — aman diinjak banyak thread, dengan syarat isinya hanya bisa dibaca. Arc::clone murah karena hanya menggerakkan penghitung, dan data dijatuhkan tepat saat pemilik terakhir pergi. Kombinasi Arc<Mutex<T>> membagikan brankas sekaligus menjaga antrean isinya: pola baku untuk penghitung bersama dan status yang diubah banyak thread. Meski demikian, satu pertanyaan mendasar belum kita jawab: dari mana Rust tahu tipe mana yang boleh dan tidak boleh menyeberang thread? Jawabannya ada pada sepasang marker trait — Send dan Sync — Chapter berikutnya.