Topik ini membahas scoped thread, yaitu thread yang dibuat di dalam lingkup thread::scope sehingga boleh meminjam data lokal tanpa Arc dan tanpa Mutex. Pembahasan mencakup alasan spawn biasa mensyaratkan 'static, jaminan penggabungan yang diberikan scope sebelum lingkupnya berakhir, serta pemakaian pinjaman baca dan pinjaman tulis yang tidak tumpang tindih lewat split_at_mut. Setelah membaca topik ini, data milik thread utama dapat dipinjamkan ke banyak thread dengan aman tanpa pembungkus sinkronisasi.
Batasan ‘static pada spawn#
spawn tidak mengetahui berapa lama thread yang dibuatnya akan hidup. Pegangan JoinHandle dapat dijatuhkan begitu saja dan thread tetap berjalan, sehingga dari sudut pandang pemeriksa tipe, thread itu dapat hidup melewati pemanggil, melewati fungsi pembuatnya, bahkan sampai akhir proses. Satu-satunya umur yang mencakup semua kemungkinan itu adalah 'static, dan karena itu closure beserta seluruh tangkapannya wajib mencukupi umur tersebut.
Akibatnya data lokal tidak dapat dipinjamkan ke thread hasil spawn: nilai harus dipindahkan dengan move, atau kepemilikan bersamanya dibangun lewat Arc. Kompilasi program berikut menunjukkan penolakan atas pinjaman slice yang hendak dibawa ke thread, meskipun join dipanggil pada baris berikutnya.
export PATH="$HOME/.cargo/bin:$PATH"
# pinjaman slice lokal dibawa ke thread hasil spawn dan ditolak kompilator
printf 'use std::thread;\n\nfn main() {\n let data = vec![10, 24, 100];\n let potongan = &data[..];\n\n let utas = thread::spawn(move || println!("banyak elemen: {}", potongan.len()));\n utas.join().unwrap();\n}\n' > pinjam-spawn.rs
# tampilkan bagian utama pesan error kompilasi
rustc --edition=2021 pinjam-spawn.rs 2>&1 | sed -n '1,13p'error[E0597]: `data` does not live long enough
--> pinjam-spawn.rs:5:21
|
4 | let data = vec![10, 24, 100];
| ---- binding `data` declared here
5 | let potongan = &data[..];
| ^^^^ borrowed value does not live long enough
6 |
7 | let utas = thread::spawn(move || println!("banyak elemen: {}", potongan.len()));
| -------------------------------------------------------------------- argument requires that `data` is borrowed for `'static`
8 | utas.join().unwrap();
9 | }
| - `data` dropped here while still borrowedError E0597 menunjuk spawn sebagai pihak yang mensyaratkan pinjaman 'static, dan join pada baris berikutnya tidak menolong karena pemeriksa umur tidak melihat jadwal thread, hanya batasan tanda tangan spawn. Pemahaman atas batas inilah yang menyiapkan pemakaian scope, karena scope mengganti janji umur yang tidak diketahui dengan janji umur yang dapat diperiksa.
thread::scope: meminjam dengan aman#
Fungsi thread::scope menerima sebuah closure yang parameternya bertipe Scope. Thread dibuat lewat lingkup.spawn, dan closure yang dikirim ke sana tidak lagi disyaratkan 'static, melainkan hanya harus hidup selama lingkup scope itu sendiri. Pemeriksa pinjaman kemudian memastikan setiap data yang dipinjam thread benar-benar hidup lebih lama daripada scope, karena scope tidak akan kembali sebelum seluruh thread di dalamnya selesai dan digabungkan.
Program berikut meminjamkan array lokal ke dua thread tanpa move, tanpa Arc, dan tanpa kunci. Hasil tiap thread diambil lewat join, lalu dikembalikan dari closure scope sebagai nilai kembalian biasa.
use std::thread;
fn main() {
let data = [3, 8, 21, 5, 13, 34];
let ambang = 3;
let (jumlah_bawah, jumlah_atas) = thread::scope(|lingkup| {
let bawah = lingkup.spawn(|| data[..ambang].iter().sum::<i32>());
let atas = lingkup.spawn(|| data[ambang..].iter().sum::<i32>());
(bawah.join().unwrap(), atas.join().unwrap())
});
println!("jumlah bawah : {}", jumlah_bawah);
println!("jumlah atas : {}", jumlah_atas);
println!("total : {}", jumlah_bawah + jumlah_atas);
}jumlah bawah : 32
jumlah atas : 52
total : 84Kedua closure meminjam data dan ambang sebagaimana closure biasa meminjam variabel sekitar, karena batasan umur pada lingkup.spawn mengikuti lingkup, bukan 'static. Nilai 32 dan 52 dihitung di dua thread yang berbeda, dan pasangan hasilnya keluar dari scope sebagai nilai kembalian closure. Penggabungan seluruh thread terjadi secara otomatis pada akhir closure scope, sebelum data dan ambang berpeluang dijatuhkan. Diagram berikut merangkum hubungan masa hidupnya.
Pinjaman tulis yang tidak tumpang tindih#
Manfaat terbesar scope tampak pada pinjaman tulis. Metode split_at_mut membelah satu potongan mutable menjadi dua potongan yang wilayahnya tidak saling mengenai, dan aturan pinjaman menerima dua pinjaman tulis tersebut karena keduanya menggarap elemen yang berbeda. Tiap potongan kemudian dipinjamkan ke satu thread, sehingga dua thread mengubah satu Vec pada saat bersamaan tanpa Mutex.
Program berikut mengalikan sepuluh kali pada separuh kiri dan seratus kali pada separuh kanan dari data yang sama, serentak di dua thread.
use std::thread;
fn main() {
let mut data = vec![1, 2, 3, 4, 5, 6];
let (kiri, kanan) = data.split_at_mut(3);
thread::scope(|lingkup| {
lingkup.spawn(|| {
for nilai in kiri.iter_mut() {
*nilai *= 10;
}
});
lingkup.spawn(|| {
for nilai in kanan.iter_mut() {
*nilai *= 100;
}
});
});
println!("data hasil: {:?}", data);
}data hasil: [10, 20, 30, 400, 500, 600]Pemisahan tugas per wilayah membuat “kunci” tidak diperlukan sama sekali: borrow checker membuktikan pada saat kompilasi bahwa kedua thread tidak pernah menyentuh elemen yang sama, sehingga tidak ada yang perlu diantrekan. Skema ini bekerja untuk pembagian statis seperti pengolahan array per bagian, dan pola yang sama berlaku untuk chunks_mut serta untuk pemetaan beberapa struktur terpisah ke thread yang berbeda. Data yang pembagiannya tidak dapat ditetapkan sejak awal tetap memerlukan Mutex, karena dua thread yang menentukan wilayahnya masing-masing pada saat berjalan dapat mendarat di elemen yang sama.
Kesalahan umum#
Kesalahan pertama adalah menganggap scope menghapus risiko penantian. Scope menggabungkan seluruh thread tanpa syarat, sehingga satu thread yang menunggu pesan yang tidak pernah dikirim, misalnya dari thread utama yang sibuk menunggu scope selesai, membuat program berhenti selamanya. Pastikan setiap penantian di dalam scope memiliki pengirim yang pasti selesai, atau akhiri penantian itu dengan penutupan channel secara eksplisit.
Kesalahan kedua adalah membuang ScopedJoinHandle yang nilai kembaliannya dibutuhkan. Thread tetap dijalankan dan tetap digabungkan oleh scope, tetapi hasil closure-nya ikut terbuang bersama pegangan. Panggil join pada setiap pegangan yang hasilnya dipakai, sebagaimana pemakaian join pada spawn biasa.
Kesalahan ketiga adalah mengakses data dari thread utama di tengah scope sementara data itu masih dipinjam thread lain, misalnya mencetak data di dalam closure scope setelah kedua thread dibuat. Penolakannya datang dari borrow checker sebagai konflik pinjaman biasa, bukan dari runtime, dan perbaikannya adalah menunda akses hingga scope berakhir sehingga seluruh pinjaman selesai.
Ringkasan#
spawnmensyaratkan'statickarena umur thread tidak diketahui, sehingga data lokal hanya dapat dipindahkan atau dibungkusArc, dan pinjaman ditolak dengan error E0597.thread::scopemengganti syarat itu dengan lingkup: thread di dalamnya boleh meminjam data lokal, dan seluruh thread digabungkan otomatis sebelum scope kembali.- Hasil thread di dalam scope diambil lewat
joinpadaScopedJoinHandledan dapat dikembalikan sebagai nilai kembalian closure scope. split_at_mutbeserta kerabatnya memungkinkan dua thread mengubah bagian data yang berbeda tanpa kunci, karena borrow checker membuktikan ketiadaan tumpang tindih saat kompilasi.
Referensi#
- thread::scope — Rust standard library — Dokumentasi fungsi scope beserta jaminan penggabungan thread sebelum lingkup berakhir.
- thread::Scope — Rust standard library — Dokumentasi tipe Scope mengenai spawn yang boleh meminjam data lingkup.
- ScopedJoinHandle in std::thread — Rust standard library — Dokumentasi pegangan thread scope mengenai join dan nilai kembaliannya.