Arc dan Mutex

Kepemilikan bersama lintas thread serta pratinjau Send dan Sync.

Daftar isi

Topik ini membahas Arc<T> dan Mutex<T>, yaitu pasangan tipe untuk kepemilikan bersama lintas thread: Arc menghitung pemilik secara atomik sehingga data dapat dimiliki banyak thread, dan Mutex mengantrekan akses tulis sehingga perubahan data tidak bertabrakan. Pembahasan mencakup alasan Rc tertolak saat dikirim ke thread, pemakaian Arc bersama std::thread::spawn, penguncian data dengan Mutex, serta pratinjau trait Send dan Sync yang menjadi dasar pemeriksaannya. Setelah membaca topik ini, data dapat dibagi dengan aman ke beberapa thread, dan dasar bagi pembahasan thread secara penuh telah tersedia.

Kepemilikan bersama lintas thread#

Rc tidak dapat keluar dari thread yang membuatnya. Alasannya berada pada penghitungnya: pembaruan hitungan Rc adalah operasi biasa yang tidak atomik, sehingga dua thread yang menaikkan atau menurunkan hitungan pada saat bersamaan dapat saling menimpa hasilnya. Hitungan yang rusak berujung pada pembebasan ganda atau memori yang tidak pernah dibebaskan. std::thread::spawn mencegah keadaan itu sejak kompilasi dengan mensyaratkan closure yang dikirim mengimplementasikan Send, dan Rc<String> tidak memenuhi syarat tersebut. Penolakannya dapat diperiksa langsung dengan rustc.

bash
export PATH="$HOME/.cargo/bin:$PATH"
# contoh kode yang ditolak kompilator: Rc dikirim ke thread lain
printf 'use std::rc::Rc;\nuse std::thread;\n\nfn main() {\n    let data = Rc::new(String::from("sensor"));\n    let salinan = Rc::clone(&data);\n\n    thread::spawn(move || {\n        println!("{}", salinan);\n    });\n}\n' > salah.rs
rustc --edition=2021 salah.rs 2>&1 | sed -n '1,14p'
text Keluaran
error[E0277]: `Rc<String>` cannot be sent between threads safely
  --> salah.rs:8:19
   |
 8 |       thread::spawn(move || {
   |       ------------- ^------
   |       |             |
   |  _____|_____________within this `{[email protected]:8:19: 8:26}`
   | |     |
   | |     required by a bound introduced by this call
 9 | |         println!("{}", salinan);
10 | |     });
   | |_____^ `Rc<String>` cannot be sent between threads safely
   |
   = help: within `{[email protected]:8:19: 8:26}`, the trait `Send` is not implemented for `Rc<String>`

Baris the trait Send is not implemented for Rc<String> menyatakan inti penolakannya, dan baris lainnya menunjuk closure pada thread::spawn sebagai titik pemeriksaannya. Keluaran dipotong pada baris ke-14, dan pesan aslinya masih berlanjut dengan catatan penelusuran sampai definisi spawn. Penolakan ini bukan hambatan melainkan pengarahan: kebutuhan kepemilikan bersama lintas thread dilayani oleh Arc<T>, yaitu tipe dengan antarmuka yang sama seperti Rc tetapi dengan penghitungan yang diperbarui secara atomik.

Arc: penghitungan referensi atomik#

Arc adalah singkatan dari atomically reference counted. Antarmukanya sama dengan Rc: Arc::new membuat alokasi, Arc::clone menambah pemilik tanpa menyalin data, dan Arc::strong_count melaporkan jumlah pemilik aktif. Perbedaannya terletak pada pembaruan hitungan yang memakai operasi atomik, sehingga beberapa thread dapat menaikkan dan menurunkan hitungan pada saat bersamaan tanpa saling menimpa. Biaya operasi atomik menjadikan Arc sedikit lebih mahal daripada Rc, sehingga Rc tetap dipakai selama data tidak pernah menyeberang thread.

Program berikut membagi satu teks ke dua thread. Setiap thread menerima satu Arc::clone, menjalankan pekerjaan, lalu mengembalikan hasilnya lewat nilai kembalian closure yang ditunggu pemanggil dengan join.

rust arc-sederhana.rs
use std::sync::Arc;
use std::thread;

fn main() {
    let teks = Arc::new(String::from("log sensor harian"));

    let pegangan1 = Arc::clone(&teks);
    let utas1 = thread::spawn(move || pegangan1.len());

    let pegangan2 = Arc::clone(&teks);
    let utas2 = thread::spawn(move || pegangan2.to_uppercase());

    println!("pemilik sebelum join : {}", Arc::strong_count(&teks));
    println!("panjang              : {}", utas1.join().unwrap());
    println!("huruf besar          : {}", utas2.join().unwrap());
    println!("pemilik setelah join : {}", Arc::strong_count(&teks));
}
text Keluaran
pemilik sebelum join : 3
panjang              : 17
huruf besar          : LOG SENSOR HARIAN
pemilik setelah join : 1

Keluaran tersebut menunjukkan hitungan pemilik yang naik menjadi tiga saat kedua thread memegang masing-masing Arc, lalu kembali menjadi satu setelah kedua join selesai dan pegangan para thread dijatuhkan. Seluruh pencetakan dilakukan dari main setelah nilai-nilai diterima kembali, sehingga urutannya pasti dan tidak bergantung pada jadwal thread. Pemakaian join memang menjadi bagian dari kontrak pola ini, sebagaimana ditegaskan pada callout berikut.

Mutex: akses tulis yang diantrekan#

Berbagi data secara baca saja jarang memadai, dan dua thread yang menulis satu nilai pada saat bersamaan merupakan data race. Mutex<T> mencegah tabrakan itu dengan mengantrekan akses: hanya satu pemegang kunci yang boleh menyentuh data pada satu waktu, dan pemegang berikutnya menunggu hingga kunci dilepas. Metode lock mengembalikan Result berisi MutexGuard<T>, dan penjaga ini membuka akses ke data selagi kuncinya terpasang, kemudian melepas kunci secara otomatis ketika penjaga itu keluar dari lingkupnya.

Program berikut membagi satu Arc<Mutex<i32>> ke sepuluh thread yang masing-masing menambah lima ke nilai bersama. Karena seluruh pencetakan dilakukan setelah semua thread selesai, nilainya pasti lima puluh berapa pun urutan eksekusinya.

rust hitung-bersama.rs
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let hitung = Arc::new(Mutex::new(0));
    let mut pegangan = Vec::new();

    for _ in 0..10 {
        let hitung = Arc::clone(&hitung);
        pegangan.push(thread::spawn(move || {
            let mut angka = hitung.lock().unwrap();
            *angka += 5;
        }));
    }

    for utas in pegangan {
        utas.join().unwrap();
    }

    println!("total: {}", *hitung.lock().unwrap());
}
text Keluaran
total: 50

Diagram berikut merangkum kerja sama kedua tipe pada program tersebut: Arc membagi satu kepemilikan ke banyak thread, dan Mutex menjaga satu pintu menuju data yang dibagikan.

utas 1utas 2utas 3Architungan atomikMutexsatu kuncidatanilai bersamaArc membagi kepemilikan, Mutex mengantrekan akses tulis ke data
Arc membagi kepemilikan lintas thread, Mutex mengantrekan akses tulis ke data bersama

Susunan Arc<Mutex<T>> pada contoh memiliki urutan yang bukan kebetulan. Mutex berada di dalam agar yang dibagikan adalah data yang terkunci, bukan sekadar penunjuknya, dan Arc membungkus dari luar agar banyak thread dapat memegang satu Mutex yang sama. Pemanggilan lock().unwrap() pada contoh menangani satu kemungkinan gagal, yaitu kunci yang berstatus poisoned karena thread sebelumnya panic saat masih memegangnya. Penjaga MutexGuard dilepas otomatis pada akhir lingkup closure, sehingga kunci tidak pernah tertinggal terkunci.

Send dan Sync sebagai pratinjau#

Dua trait penanda (marker trait) menjadi dasar seluruh pemeriksaan pada bagian-bagian sebelumnya. Trait Send menyatakan bahwa kepemilikan sebuah nilai boleh dipindahkan ke thread lain, sedangkan Sync menyatakan bahwa nilai itu boleh diakses lewat referensi baca dari beberapa thread sekaligus. Keduanya tidak memiliki metode dan diimplementasikan otomatis oleh kompilator berdasarkan isi tipe, sehingga pemeriksaannya tetap berlangsung saat kompilasi meskipun program berjalan paralel.

Beberapa keputusan pada topik ini dapat dibaca ulang melalui kedua trait tersebut. Rc<T> tidak Send maupun Sync karena penghitungnya tidak atomik, dan itulah alasan penolakan E0277 pada bagian pertama. Arc<T> bersifat Send dan Sync hanya apabila T juga demikian, sehingga Arc<String> lolos sedangkan Arc<Rc<String>> tetap tertolak. RefCell<T> bersifat Send tetapi tidak Sync karena gerbang pinjamannya hanya aman dalam satu thread, dan Mutex<T> menghadirkan Sync atas data yang Send dengan jaminan satu penulis pada satu waktu. Pembahasan penuh kedua trait tersebut beserta data race berada pada modul konkurensi.

Kesalahan umum#

Kesalahan pertama adalah memanggil lock dua kali pada Mutex yang sama dari satu thread, misalnya karena hasil lock disimpan lalu metode lain memanggil lock lagi. Pemanggilan kedua menunggu kunci yang masih dipegang pemanggilan pertama pada thread yang sama, sehingga thread berhenti menunggu selamanya dalam keadaan yang disebut deadlock. Perbaikannya adalah memegang penjaga sesempit mungkin: ambil data yang dibutuhkan, lepaskan penjaga dengan keluar dari bloknya, dan hindari memanggil kode lain yang tidak diketahui keadaan kuncinya selagi masih memegang penjaga.

Kesalahan kedua adalah menukar urutan pembungkus menjadi Mutex<Arc<T>> dengan harapan data tetap dapat diubah. Urutan itu hanya melindungi penunjuk Arc, sedangkan data di bawahnya tetap hanya dapat dipinjam baca, sehingga penulisan data tetap ditolak kompilator. Bentuk yang benar untuk data bersama yang dapat diubah adalah Arc<Mutex<T>>, karena Mutex yang dibagikan menghadirkan akses tulis yang diantrekan atas data itu sendiri.

Ringkasan#

  • Arc<T> menghitung pemilik secara atomik sehingga kepemilikan bersama dapat menyeberang thread, dengan antarmuka yang sama seperti Rc dan biaya operasi atomik yang sedikit lebih tinggi.
  • Rc<T> ditolak thread::spawn dengan error E0277 karena tidak mengimplementasikan Send, dan JoinHandle dari setiap thread perlu ditunggu dengan join.
  • Mutex<T> mengantrekan akses tulis lewat MutexGuard yang melepas kunci secara otomatis saat keluar lingkup, dan Arc<Mutex<T>> adalah bentuk baku data bersama yang dapat diubah.
  • Trait penanda Send dan Sync diperiksa saat kompilasi dan menjadi dasar penolakan seluruh kombinasi tipe yang tidak aman lintas thread.

Referensi#