Topik ini membahas Send dan Sync, dua marker trait yang menjadi dasar seluruh pemeriksaan keamanan thread pada Rust, mulai dari makna masing-masing, penurunan keduanya oleh kompilator, sampai alasan kombinasi tipe tertentu ditolak sebelum program berjalan. Pembahasan mencakup definisi data race, aturan penurunan Send dan Sync atas tipe bentukan, serta cara membaca pesan error E0277 sampai ke penyebabnya. Setelah membaca topik ini, penolakan seperti Rc yang tidak dapat menyeberang thread dapat dijejak sampai mekanismenya, dan pemilihan tipe untuk program konkuren dapat dinilai dari kedua trait ini.
Data race dan jaminan Rust#
Data race terjadi ketika dua thread mengakses lokasi memori yang sama pada saat bersamaan, paling tidak satu aksesnya adalah penulisan, dan tidak ada sinkronisasi yang mengatur urutan keduanya. Akibatnya beragam dan semuanya buruk: nilai yang terbaca dapat berupa campuran dua penulisan, penulisan yang hilang, atau kerusakan struktur data yang lebih dalam. Pada banyak bahasa, keadaan seperti itu masuk kategori perilaku tak terdefinisi (undefined behavior), dan pencariannya berlangsung setelah program salah jalan di lingkungan nyata.
Rust menegakkan jaminan yang lebih ketat untuk kode aman: data race mustahil terjadi, dan pemeriksaannya berlangsung saat kompilasi, bukan saat program berjalan. Penegakannya memakai dua trait penanda yang bekerja pada batas thread, dan karena keduanya berupa tipe, seluruh alat tipe Rust ikut bekerja, mulai dari pesan error sampai batasan generik. Harga jaminan itu berupa pembatasan: sebagian kombinasi tipe yang lazim pada bahasa lain tidak boleh dipakai lintas thread, dan tipe pengganti yang tepat harus dipilih secara sadar.
Ketiga syarat data race dipatahkan oleh aturan yang sudah dikenal. Aturan pinjaman mencegah dua pegangan yang bertentangan atas data yang sama dalam satu thread, dan kunci Mutex atau RwLock memperluas jaminan itu lintas thread. Sisanya, yakni batas tempat nilai atau referensi berpindah ke thread lain, diperiksa oleh Send dan Sync yang dibahas pada dua bagian berikut.
Send: memindahkan kepemilikan antar thread#
Send adalah trait penanda untuk tipe yang kepemilikannya boleh dipindahkan ke thread lain. Tidak ada satu pun metode di dalamnya, sehingga statusnya murni pernyataan yang dipercaya pemeriksa tipe. Perpindahan itu terjadi pada move di closure spawn, pada pengiriman channel, dan pada nilai kembalian join dari thread pekerja.
Hampir semua tipe Rust adalah Send: i32, String, Vec<T>, dan Box<T> masuk kelompok ini karena hanya berisi data milik sendiri. Tipe bentukan diturunkan secara otomatis: struct atau enum menjadi Send apabila seluruh medannya Send, tanpa anotasi apa pun. Yang menonjol justru pengecualiannya, misalnya Rc<T> yang bukan Send karena penghitung referensinya tidak diperbarui secara atomik, serta pointer mentah *const T dan *mut T yang tidak membawa jaminan apa pun. Penolakan Rc oleh spawn dengan error E0277 telah ditunjukkan pada topik Arc dan Mutex, sehingga pembahasan di sini berlanjut ke sisi yang belum tersentuh.
Persyaratan itu tampak langsung pada tanda tangan spawn di pustaka standar:
// tidak-dijalankan
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,Batasan F: Send menyatakan closure beserta seluruh nilai yang ditangkapnya harus dapat dikirim, dan batasan T: Send menjaga nilai hasil agar dapat dikirim kembali kepada thread pemanggil. Karena batasan berupa trait, fungsi buatan sendiri dapat memasang syarat yang sama, dan itulah dasar dari contoh pembuktian pada bagian penutup topik ini.
Sync: membagikan pinjaman baca#
Sync adalah trait penanda untuk tipe yang boleh diakses lewat referensi dari beberapa thread sekaligus. Hubungannya dengan Send ringkas: T disebut Sync apabila &T adalah Send, sehingga pengiriman pinjaman baca ke thread lain menuntut tipe asalnya bersifat Sync. Pemakaian yang paling terasa adalah Arc<T>: tipe itu hanya berguna lintas thread apabila T juga Sync, karena banyak pemegang Arc saling membagikan &T.
RefCell<T> mencontohkan tipe yang Send tetapi tidak Sync. Gerbang pinjamannya berupa pencacah biasa yang tidak diperbarui secara atomik, sehingga mutasi dari balik pinjaman baca yang aman di satu thread berubah menjadi data race bila pencacah itu diperbarui oleh banyak thread sekaligus. Kompilasi program berikut menolak pengiriman &RefCell ke thread lain.
export PATH="$HOME/.cargo/bin:$PATH"
# referensi ke RefCell dikirim ke thread lain dan ditolak kompilator
printf 'use std::cell::RefCell;\nuse std::thread;\n\nfn main() {\n let sel = RefCell::new(vec![10, 24]);\n let pinjam = &sel;\n\n thread::spawn(move || {\n println!("{:?}", pinjam.borrow());\n });\n}\n' > pinjam-sel.rs
# tampilkan bagian utama pesan error kompilasi
rustc --edition=2021 pinjam-sel.rs 2>&1 | sed -n '1,14p'error[E0277]: `RefCell<Vec<i32>>` cannot be shared between threads safely
--> pinjam-sel.rs:8:19
|
8 | thread::spawn(move || {
| _____-------------_^
| | |
| | required by a bound introduced by this call
9 | | println!("{:?}", pinjam.borrow());
10 | | });
| |_____^ `RefCell<Vec<i32>>` cannot be shared between threads safely
|
= help: the trait `Sync` is not implemented for `RefCell<Vec<i32>>`
= note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` instead
= note: required for `&RefCell<Vec<i32>>` to implement `Send`Pesan itu memuat seluruh rantai alasannya: RefCell<Vec<i32>> tidak mengimplementasikan Sync, dan status itu dibutuhkan agar &RefCell<Vec<i32>> dapat menjadi Send sehingga lolos batasan spawn. Catatan bantuan bahkan menunjuk pengganti yang tepat, yaitu RwLock, yang merupakan versi sinkron dari gerbang pinjaman yang sama. Tabel berikut merangkum status kedua trait pada tipe yang sering muncul di topik konkurensi.
| Tipe | Send | Sync | penyebab |
|---|---|---|---|
i32, String, Vec<T> | ya | ya | berisi data milik sendiri |
Rc<T> | tidak | tidak | penghitung referensi tidak atomik |
Arc<T> | bila T demikian | bila T demikian | pembagian &T mengikuti isi |
RefCell<T> | ya | tidak | pencacah pinjaman tidak atomik |
Mutex<T> | ya | bila T Send | kunci menyelaraskan seluruh akses |
Diagram berikut merangkum pembagian tugas kedua trait pada batas thread: Send mengawal perpindahan kepemilikan, sedangkan Sync mengawal pembagian pinjaman baca.
Penurunan otomatis dan bukti pada kompilasi#
Kedua trait tidak diimplementasikan secara manual untuk tipe biasa. Kompilator menurunkan keduanya dari susunan tipe, dan pengecualian dinyatakan secara eksplisit oleh pustaka standar, misalnya !Send untuk Rc. Pemakaian unsafe impl Send atau unsafe impl Sync memang dimungkinkan untuk tipe buatan sendiri yang menjamin keamanannya dengan cara sendiri, tetapi penulisan itu memindahkan seluruh beban pembuktian kepada penulis tipe.
Status sebuah tipe dapat dibuktikan tanpa menjalankan program apa pun, yaitu dengan fungsi generik yang memasang kedua trait sebagai batasan. Program berikut mendefinisikan pastikan_send_sync, memanggilnya untuk beberapa tipe, lalu mencetak isi nilai yang lolos.
use std::sync::{Arc, Mutex};
struct Catatan {
nama: String,
nilai: Vec<i32>,
}
// bukti pada saat kompilasi: pemanggilan hanya lolos bila T Send dan Sync
fn pastikan_send_sync<T: Send + Sync>(nilai: T) -> T {
nilai
}
fn main() {
let catatan = pastikan_send_sync(Catatan {
nama: String::from("sensor atap"),
nilai: vec![30, 31, 29],
});
let kunci = pastikan_send_sync(Mutex::new(100));
let pegangan = pastikan_send_sync(Arc::new(String::from("log harian")));
println!("nama : {}", catatan.nama);
println!("nilai : {:?}", catatan.nilai);
println!("kunci : {:?}", kunci.lock().unwrap());
println!("pegangan: {}", pegangan);
}nama : sensor atap
nilai : [30, 31, 29]
kunci : 100
pegangan: log harianCatatan lolos tanpa deklarasi apa pun karena kedua medannya Send dan Sync, dan status itu diturunkan secara otomatis. Penggantian satu medan menjadi Rc<String> cukup untuk menggagalkan seluruh pemanggilan, dan error yang muncul tetap berupa E0277 yang menunjuk medan penyebabnya. Teknik batasan generik semacam ini juga dipakai pustaka untuk memasang syarat keamanan thread pada API publiknya tanpa menjalankan pemeriksaan tambahan saat runtime.
Kesalahan umum#
Kesalahan pertama adalah menyelundupkan Rc ke dalam Arc dengan harapan pembungkusan memperbaiki keamanannya. Arc<T> hanya Send dan Sync apabila T juga demikian, sehingga Arc<Rc<String>> tetap ditolak dengan error yang sama. Ganti sumber kepemilikan bersamanya sekaligus, misalnya Arc<Mutex<String>> untuk data yang dapat diubah.
Kesalahan kedua adalah menganggap Mutex menghadirkan Sync untuk isi apa pun. Mutex<T> hanya Sync apabila T adalah Send, karena kunci menyelaraskan akses tetapi tetap memindahkan nilai ke thread pemegang. Mutex<Rc<i32>> dengan demikian tetap tidak dapat dibagikan, dan pilihan isinya tetap diawasi kedua trait.
Kesalahan ketiga adalah berhenti pada baris pertama pesan E0277. Baris pertama hanya menyebut tipe dan trait yang kurang, sedangkan rantai penyebabnya berada pada baris required for di bagian bawah pesan. Bacalah pesan sampai baris tersebut sebelum mengubah kode, karena titik perbaikan yang tepat sering kali bukan tipe yang disebut lebih dahulu, melainkan pegangan yang mengarah kepadanya.
Ringkasan#
Sendmenyatakan kepemilikan tipe boleh dipindahkan ke thread lain, sedangkanSyncmenyatakan&Tboleh dibagikan, dan keduanya diturunkan otomatis dari susunan tipe tanpa metode apa pun.- Data race dicegah saat kompilasi: batasan
Senddan'staticpadaspawnmemeriksa setiap nilai yang menyeberang thread, dengan penolakan berupa error E0277 yang dapat dijejak sampai penyebabnya. RcdanRefCellmencontohkan pengecualian:RctidakSendmaupunSync, sedangkanRefCellSendtetapi tidakSync, dan keduanya memiliki padanan sinkron berupaArcdanRwLock.- Fungsi generik dengan batasan
T: Send + Syncmembuktikan status tipe buatan sendiri pada saat kompilasi tanpa biaya runtime.
Referensi#
- Send in std::marker — Rust standard library — Definisi trait Send beserta daftar implementasi dan pengecualiannya.
- Sync in std::marker — Rust standard library — Definisi trait Sync beserta hubungannya dengan Send melalui referensi.
- Send and Sync — The Rustonomicon — Penjelasan kedua trait dari sisi jaminan keamanan memori dan pemakaian unsafe impl.
- Extensible Concurrency with the Sync and Send Traits — The Rust Programming Language — Bab resmi yang menempatkan Send dan Sync dalam konteks konkurensi.