Sepanjang section ini kita berkali-kali menabrak tembok yang sama dengan bentuk berbeda: closure tanpa move ditolak spawn, Rc ditolak spawn, &String non-'static ditolak spawn. Semua penolakan itu bermuara pada satu mekanisme yang belum kita buka: Rust mengklasifikasikan setiap tipe dengan dua marker trait — Send dan Sync — dan memeriksa keduanya di setiap batas thread. Inilah alasan Rust disebut memiliki fearless concurrency: batas antar thread menjadi titik pemeriksaan kompilator, bukan medan saling percaya antar programmer. Chapter ini membedah kedua marker tersebut, membuktikan penolakan Rc dengan galat aslinya, lalu memetakan tipe-tipe sehari-hari pada peta Send/Sync.
Marker Trait tanpa Perilaku #
Send dan Sync adalah marker trait: keduanya tidak punya metode sama sekali — tidak ada fn yang perlu diimplementasikan. Kegunaannya bukan menambah perilaku, melainkan mencantumkan garansi: Send pada tipe T menyatakan “kepemilikan nilai T aman dipindahkan ke thread lain”, dan Sync menyatakan “reference ke T aman dikirim ke thread lain”. Tidak adanya metode membuat marker tidak bisa dipalsukan lewat implementasi yang salah — implementasi manual hanya mungkin lewat unsafe, karena programmer sendiri yang menanggung risiko menandai garansi yang tidak bisa diverifikasi kompilator.
Kedua marker ini hampir selalu dijalatkan kompilator secara otomatis berdasarkan isi tipe — sifatnya auto trait. Sebuah struct menjadi Send bila seluruh field-nya Send; sebuah Vec<T> menjadi Send bila T adalah Send; Mutex<i32> adalah Sync karena komponennya menjamin akses eksklusif. Penyusup tunggal seperti Rc membuat seluruh struct yang memuatnya kehilangan gelar Send — dan penolakannya baru terasa di spawn, jauh dari tempat struct disusun.
Send: Kepemilikan Boleh Pindah Thread #
Definisi kerjanya begini: T: Send berarti nilai bertipe T boleh dipindahkan kepemilikannya dari satu thread ke thread lain. Koneksi ke spawn langsung terlihat pada signature-nya — closure yang dikirim harus Send (dan 'static), karena nilai closure akan berpindah dari thread pemanggil ke thread baru. Closure menjadi Send bila semua yang ditangkapnya Send; itulah mengapa closure yang menangkap Rc ditolak di titik ini.
Mengapa sebagian tipe tidak layak Send? Alasannya selalu berbunyi sama: ada bagian internal yang rusak bila dioperasikan dari dua thread sekaligus tanpa perlindungan. Rc adalah contoh terbaik karena kita sudah membedahnya — penghitung pemiliknya dinaikkan dengan penjumlahan biasa. Dua thread yang meng-clone hampir bersamaan bisa sama-sama membaca penghitung lama lalu sama-sama menulis angka yang sama, sehingga satu penambahan hilang. Penghitung yang salah berarti data di-drop padahal masih ada pemilik — use after free yang nyata. Pilihan desainnya tegas: daripada memperbaiki Rc dengan biaya atomic untuk semua pengguna satu-thread, std menyediakan Arc sebagai kembaran yang aman.
Sync: Reference Boleh Dikirim #
Send bicara tentang kepemilikan; Sync bicara tentang pinjaman. Definisi formalnya ringkas: T: Sync berarti &T adalah Send — reference ke T boleh dikirim ke thread lain, yang berarti banyak thread boleh memegang reference bersamaan tanpa saling merusak. Hubungan dua marker ini terlihat pada aturan komposisi: &T menjadi Send bila T: Sync, dan &mut T menjadi Send bila T: Send — reference mutable hanya boleh ada satu, jadi yang dituntut adalah keamanan kepemilikan.
Reference memang bisa menyeberang thread dengan satu syarat ganda: tipe yang ditunjuk harus Sync, dan lifetime reference harus static — karena thread baru mungkin hidup lebih lama daripada scope pembuatnya, seperti pelajaran E0373 di Chapter Closure move di Thread. Contoh paling bersihnya adalah string literal, yang tertanam di binary program sehingga berumur selama proses:
use std::thread;
fn main() {
let pesan: &'static str = "Server kasir siap";
let pekerja = thread::spawn(move || {
format!("{} - pemeriksaan selesai", pesan)
});
println!("{}", pekerja.join().unwrap());
}Output yang Dihasilkan:
Server kasir siap - pemeriksaan selesaiMari kita bedah kode di atas:
- Baris 4 (
let pesan: &'static str = "Server kasir siap";): string literal bertipe&'static str— penunjuk ke data di dalam binary, bukan alokasi heap. Umurnya dijamin sepanjang proses berjalan. - Baris 6 (
let pekerja = thread::spawn(move || {): closure menangkappesan— tipe tangkapannya&'static stryangSendkarenastradalahSync(membaca teks bersamaan dari banyak thread aman). Tuntutan'staticdarispawnterpenuhi tanpa perlu memindahkan kepemilikan data apa pun. - Baris 7 (
format!(...)): thread membaca isi penunjuk lalu membangunStringbaru miliknya sendiri.Stringhasil format menjadi nilai kembalianJoinHandle.
Contoh ini adalah wajah Sync di dunia nyata: data dibagikan tanpa disalin, karena tipe datanya memberi jaminan bahwa pembacaan paralel tidak merusak apa pun.
Rc Ditolak, Arc Diterima #
Sekarang bukti galatnya. Program berikut menangkap Rc<String> ke closure move — kepemilikan Rc benar-benar berpindah ke thread, tidak ada pinjaman yang melanggar 'static, namun kompilator tetap menolak:
use std::rc::Rc;
use std::thread;
fn main() {
let laporan = Rc::new(String::from("Laporan Keuangan Q1"));
let pekerja = thread::spawn(move || {
println!("Memproses {}", laporan);
});
pekerja.join().unwrap();
}Kompilasi gagal dengan pesan asli berikut:
error[E0277]: `Rc<String>` cannot be sent between threads safely
--> main.rs:7:33
|
7 | let pekerja = thread::spawn(move || {
| ------------- ^------
| | |
| ___________________|_____________within this `{[email protected]:7:33: 7:40}`
| | |
| | required by a bound introduced by this call
8 | | println!("Memproses {}", laporan);
9 | | });
| |_____^ `Rc<String>` cannot be sent between threads safely
|
= help: within `{[email protected]:7:33: 7:40}`, the trait `Send` is not implemented for `Rc<String>`
note: required because it's used within this closure
--> main.rs:7:33
|
7 | let pekerja = 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`.Pesan E0277 menegaskan intinya pada satu kalimat help: the trait Send is not implemented for Rc<String>. Jejaknya juga informatif — kompilator menelusuri bahwa Rc<String> dipakai di dalam closure (Baris 8), closure inilah yang diminta Send oleh bound pada spawn, dan spawn menolaknya karena closure tidak memenuhi syarat. Perbaikannya mengganti mesin penghitung: Rc diganti Arc, kelayakan Send pulih seketika:
use std::sync::Arc;
use std::thread;
fn main() {
let laporan = Arc::new(String::from("Laporan Keuangan Q1"));
let pekerja = thread::spawn(move || {
println!("Memproses {}", laporan);
});
pekerja.join().unwrap();
println!("Selesai");
}Output yang Dihasilkan:
Memproses Laporan Keuangan Q1
SelesaiDua program di atas hanya berbeda satu kata — Rc::new menjadi Arc::new — dan itulah seluruh perbedaan antara !Send dan Send pada kasus nyata. Perlu dicatat bahwa Rc bukan satu-satunya warga bergelar buruk: RefCell<T> adalah Send (boleh dipindahkan) tetapi bukan Sync — flag peminjaman-nya juga non-atomic, sehingga mengirim &RefCell ke thread lain ditolak. Membagikan interior mutability satu-thread ke banyak thread menuntut penggantinya yang thread-aware: Mutex<T>.
Peta Send dan Sync Tipe Sehari-hari #
| Tipe | Send | Sync | Penyebab |
|---|---|---|---|
i32, f64, bool | ya | ya | Nilai tersalin utuh; tanpa keadaan bersama |
String, Vec<T> | ya | ya | Kepemilikan tunggal atas data heap |
Box<T> | jika T Send | jika T Sync | Hanya pembungkus kepemilikan |
Rc<T> | tidak | tidak | Penghitung non-atomic |
RefCell<T> | jika T Send | tidak | Flag pinjaman non-atomic |
Mutex<T> | jika T Send | jika T Send | Kunci menjamin akses eksklusif |
Arc<T> | jika T Send dan T Sync | jika T Send dan T Sync | Penghitung atomic; bagian hanya-baca |
Tabel di atas terbaca dengan logika komposisi yang sudah dibahas: tipe majemuk mewarisi gelar dari komponennya, dan satu pengecualian menjatuhkan gelar keseluruhan. Ketika suatu hari galat E0277 muncul pada struct buatan sendiri, cara membacanya adalah menelusuri field mana yang membawa penyusup — biasanya Rc, RefCell, atau pointer mentah — lalu menggantinya dengan kembaran thread-aware-nya.
Ringkasan #
Send dan Sync adalah marker trait tanpa metode: Send menandai tipe yang kepemilikannya aman dipindahkan antar thread, Sync menandai tipe yang reference-nya aman dikirim — T: Sync berarti &T: Send. Kompilator mengautomatisasi keduanya lewat komposisi field, memeriksanya di batas spawn, dan menolak penyusup seperti Rc dengan E0277 lengkap beserta rantai penelusurannya. Reference non-static tetap ditolak oleh tuntutan umur, sementara &'static str lolos karena str: Sync. Dengan peta ini, seluruh pilihan di section ini — Arc pengganti Rc, Mutex pengganti RefCell — akhirnya punya satu benang merah. Satu musuh terakhir yang belum kita lawan lahir justru dari perangkat kita sendiri: dua kunci yang saling menunggu — deadlock.