Rc<T>

Kepemilikan bersama dengan penghitungan referensi.

Daftar isi

Topik ini membahas Rc<T>, yaitu smart pointer yang menghadirkan kepemilikan bersama melalui penghitungan referensi (reference counting). Pembahasan mencakup cara kerja hitungan pemilik, makna Rc::clone yang tidak menyalin data, pemakaian Rc untuk berbagi satu data di banyak tempat, serta batasan bahwa data di bawah Rc bersifat baca saja dan berada dalam satu thread. Setelah membaca topik ini, kebutuhan kepemilikan bersama dapat dipenuhi tanpa melanggar aturan kepemilikan, dan keluaran Rc::strong_count dapat dibaca sebagai peta jumlah pemilik sebuah nilai.

Kepemilikan bersama dengan penghitungan referensi#

Aturan kepemilikan menyatakan bahwa satu nilai hanya memiliki satu pemilik pada satu waktu. Sebagian data pada program nyata justru dimiliki secara wajar oleh banyak pihak, misalnya satu daftar nama yang sama dipakai dua kategori tugas, atau satu konfigurasi yang dibaca seluruh bagian program. Menyalin data ke setiap pemilik memenuhi aturan itu, tetapi membayar dua harga: penyalinan berulang untuk data yang besar, dan setiap salinan harus diubah satu per satu apabila data itu berubah. Rc<T> menawarkan jalan ketiga, yaitu membiarkan banyak nama mengaku sebagai pemilik atas satu alokasi yang sama.

Mekanismenya berupa penghitung yang menempel pada alokasi. Setiap pembuatan Rc::clone menambah satu penunjuk baru dan menaikkan hitungan, setiap pemilik yang dijatuhkan menurunkan hitungan, dan alokasi dibebaskan hanya ketika hitungan mencapai nol. Pembebasan tetap terjadi tepat satu kali sehingga keamanan memori terjaga, dan aturan kepemilikan tidak dilanggar melainkan dilonggarkan menjadi kepemilikan bersama dengan syarat baca saja. Pembatasan baca saja inilah harga yang dibayar, dan alasannya dibahas pada bagian terakhir isi topik ini.

Rc::clone menaikkan hitungan, bukan menyalin data#

Pada tipe biasa, clone menyalin data sehingga terbentuk dua salinan yang berdiri sendiri. Pada Rc, pemanggilan Rc::clone(&x) hanya membuat penunjuk baru ke alokasi yang sama dan menaikkan hitungannya satu tingkat, sehingga biayanya tidak bergantung pada besar data. Program berikut memeriksa perilaku tersebut dengan mencetak hitungan kuat sebelum, selama, dan sesudah sebuah blok yang menampung dua pemilik tambahan.

rust hitung-referensi.rs
use std::rc::Rc;

fn main() {
    let label = Rc::new(String::from("sensor-suhu"));

    println!("pemilik awal      : {}", Rc::strong_count(&label));

    {
        let salinan_a = Rc::clone(&label);
        let salinan_b = Rc::clone(&label);
        println!("di dalam blok     : {}", Rc::strong_count(&label));
        println!("semua membaca     : {} / {} / {}", label, salinan_a, salinan_b);
    } // salinan_a dan salinan_b dijatuhkan di sini

    println!("setelah blok      : {}", Rc::strong_count(&label));
    println!("label tetap hidup : {}", label);
}
text Keluaran
pemilik awal      : 1
di dalam blok     : 3
semua membaca     : sensor-suhu / sensor-suhu / sensor-suhu
setelah blok      : 1
label tetap hidup : sensor-suhu

Keluaran tersebut menampilkan siklus hidup hitungan secara utuh. Hitungan bernilai satu saat hanya variabel label yang memegang alokasi, naik menjadi tiga ketika salinan_a dan salinan_b dibuat, lalu turun kembali menjadi satu setelah kedua nama itu keluar dari bloknya dan dijatuhkan. Ketiga nama membaca teks yang sama tanpa penyalinan, dan label tetap hidup setelah blok berakhir karena tetap menjadi pemilik. Apabila hitungan terakhir turun ke nol, misalnya seluruh pemilik berada dalam satu blok, alokasi teks dibebaskan pada saat blok itu selesai.

Berbagi satu data di banyak tempat#

Pemakaian yang lazim bagi Rc adalah struct yang masing-masing menyimpan sebagian data bersama. Program berikut membangun dua struct, Panel dan Arsip, yang sama-sama menyimpan label satuan pengukuran. Label itu dibuat sekali di heap, lalu kedua struct menerima Rc::clone dari label tersebut sehingga ketiga pemilik membaca satu teks yang sama.

rust berbagi-satuan.rs
use std::rc::Rc;

struct Panel {
    nama: String,
    satuan: Rc<String>,
}

struct Arsip {
    lokasi: String,
    satuan: Rc<String>,
}

fn main() {
    let satuan = Rc::new(String::from("derajat Celsius"));

    let panel = Panel {
        nama: String::from("panel-utama"),
        satuan: Rc::clone(&satuan),
    };
    let arsip = Arsip {
        lokasi: String::from("gudang-catatan"),
        satuan: Rc::clone(&satuan),
    };

    println!("{} membaca {}", panel.nama, panel.satuan);
    println!("{} membaca {}", arsip.lokasi, arsip.satuan);
    println!("jumlah pemilik satuan: {}", Rc::strong_count(&satuan));
}
text Keluaran
panel-utama membaca derajat Celsius
gudang-catatan membaca derajat Celsius
jumlah pemilik satuan: 3

Diagram berikut merangkum keadaan memori program tersebut. Tiga nama pemilik di stack mengarah ke satu teks di heap, dan hitungan kuat bernilai tiga.

stacksatuan: Rcpanel.satuan: Rcarsip.satuan: Rc"derajat Celsius"hitungan kuat = 3Rc::clone hanya menaikkan hitungan, teks disimpan satu kali di heap
Tiga pemilik Rc atas satu teks yang sama, dengan hitungan kuat bernilai tiga

Pada versi yang menyimpan String biasa pada tiap struct, teks “derajat Celsius” tersimpan tiga kali di heap, dan setiap penambahan pemilik menyalin seluruh isi teks. Dengan Rc, penambahan pemilik hanya berarti satu penunjuk baru dan satu kenaikan hitungan, sehingga struktur yang saling berbagi data tetap murah dibangun maupun dibongkar. Pola seperti ini cocok ketika data bersama berukuran besar atau ketika jumlah pemiliknya bertambah saat program berjalan, misalnya daftar simpul yang sama muncul di beberapa daftar induk.

Data di bawah Rc bersifat baca saja#

Pinjaman tulis menuntut kepemilikan yang bersifat eksklusif, sedangkan Rc justru membagi kepemilikan kepada banyak pemilik. Kedua hal tersebut tidak dapat dipenuhi sekaligus, karena izin menulis pada satu pemilik akan membatalkan jaminan baca yang dipegang pemilik lain pada saat yang sama. Oleh sebab itu Rc<T> hanya membagikan pinjaman baca atas nilai di dalamnya, dan percobaan mengubah nilai tersebut melalui Rc ditolak saat kompilasi. Mutasi tetap mungkin dilakukan lewat Rc::get_mut yang hanya berhasil ketika hitungan kuat bernilai satu dan tidak ada Weak yang masih memegang alokasi yang sama, atau lewat kombinasi dengan RefCell<T> yang memindahkan pemeriksaan pinjaman ke runtime.

Dua batasan pada bagian ini, yaitu baca saja dan satu thread, bukan kelemahan desain melainkan konsekuensi langsung dari keamanan yang dijanjikan. Pembacaan bersama di satu thread adalah wilayah yang dapat dijamin aman tanpa alat sinkronisasi, dan Rc memanfaatkan wilayah itu sepenuhnya. Wilayah yang memerlukan mutasi dari banyak pemilik dilayani oleh kombinasi Rc dengan RefCell, sedangkan wilayah lintas thread dilayani oleh Arc bersama Mutex.

Kesalahan umum#

Kesalahan pertama adalah mengharapkan Rc memungkinkan mutasi atas nilai yang dibungkusnya. Misalnya, push_str dipanggil pada variabel bertipe Rc<String> dengan harapan teks berubah, tetapi kompilator menolaknya karena pinjaman tulis atas nilai bersama tidak mungkin diberikan. Perbaikannya bergantung pada kebutuhan: susun ulang agar satu pemilik mengubah nilai sebelum dibagikan, atau bungkus nilai dengan RefCell agar pemeriksaan pinjaman berpindah ke runtime. Pilihan pertama lebih sederhana dan sebaiknya diutamakan selama urutan kode masih masuk akal.

Kesalahan kedua adalah kebingungan ketika data di bawah Rc tidak kunjung dibebaskan padahal pemilik yang diketahui sudah habis. Kondisi ini terjadi karena masih ada pemilik lain yang menyimpan clone, misalnya struct lama yang masih hidup atau collection yang belum dikosongkan. Cetak Rc::strong_count pada beberapa titik program untuk menghitung pemilik yang tersisa, lalu telusuri siapa yang masih memegang alokasi itu. Ingat pula bahwa pemilik yang saling menahan, yaitu siklus referensi, membuat hitungan tidak pernah mencapai nol meskipun seluruh nama di luar struktur sudah lenyap.

Ringkasan#

  • Rc<T> menghadirkan kepemilikan bersama: Rc::clone menambah penunjuk dan menaikkan hitungan kuat tanpa menyalin data, dan alokasi dibebaskan saat hitungan mencapai nol.
  • Rc::strong_count melaporkan jumlah pemilik aktif dan dapat dipakai untuk menelusuri siklus hidup data bersama.
  • Nilai di bawah Rc bersifat baca saja karena pinjaman tulis menuntut kepemilikan eksklusif, sehingga mutasi memerlukan RefCell atau Rc::get_mut.
  • Rc hanya berlaku dalam satu thread karena pembaruan hitungannya tidak atomik, dan kepemilikan bersama lintas thread ditangani oleh Arc.

Referensi#