Thread hanya berguna bila membawa pekerjaan — dan pekerjaan berarti data: nomor pesanan yang harus diproses, daftar harga yang harus dihitung, laporan yang harus diaudit. Section Closure telah memperkenalkan kata kunci move sebagai alat untuk memindahkan kepemilikan ke dalam closure; Section ini menempatkannya pada panggung yang membutuhkannya paling sering: thread::spawn. Kita akan mulai dari galat asli yang pasti ditemui setiap penulis Rust — closure yang meminjam data dari main ditolak keras oleh kompilator — lalu memperbaikinya dengan move, dan menutup dengan pola clone untuk data yang dibutuhkan beberapa thread sekaligus.
Pinjaman yang Ditolak oleh spawn #
Masalahnya berakar pada umur. Thread yang diluncurkan spawn boleh hidup lebih lama daripada fungsi yang meluncurkannya: handle bisa disimpan, dipinda-pindahkan, dan ditukar join belakangan. Karena borrow checker tidak bisa membuktikan bahwa kita akan join sebelum main berakhir, signature spawn menuntut closure bertipe 'static — closure tidak boleh meminjam apa pun yang umurnya terikat fungsi pembuatnya. Kode berikut mencoba mengirim nomor_pesanan ke thread dengan pinjaman biasa:
use std::thread;
fn main() {
let nomor_pesanan = String::from("PSN-2026-0417");
let pekerja = thread::spawn(|| {
println!("Memproses pesanan {}", nomor_pesanan);
});
pekerja.join().unwrap();
println!("Selesai");
}Kompilasi gagal dengan pesan asli berikut:
error[E0373]: closure may outlive the current function, but it borrows `nomor_pesanan`, which is owned by the current function
--> main.rs:6:33
|
6 | let pekerja = thread::spawn(|| {
| ^^ may outlive borrowed value `nomor_pesanan`
7 | println!("Memproses pesanan {}", nomor_pesanan);
| ------------- `nomor_pesanan` is borrowed here
|
note: function requires argument type to outlive `'static`
--> main.rs:6:19
|
6 | let pekerja = thread::spawn(|| {
| ___________________^
7 | | println!("Memproses pesanan {}", nomor_pesanan);
8 | | });
| |______^
help: to force the closure to take ownership of `nomor_pesanan` (and any other referenced variables), use the `move` keyword
|
6 | let pekerja = thread::spawn(move || {
| ++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0373`.Pesan E0373 menuturkan konflik umurnya dengan gamblang: closure may outlive the current function — boleh hidup lebih lama dari fungsi ini — tetapi closure borrows nomor_pesanan yang dimiliki fungsi ini. Pinjaman pasti gugur ketika main berakhir, sementara thread belum tentu selesai saat itu terjadi; Rust menolak menebak nasib data. Baris help bahkan menuliskan perbaikannya huruf demi huruf: sisipkan move sebelum ||.
Perhatikan bahwa pekerja.join().unwrap() di Baris 8 tidak menolong. Penjadwalan pengecekan umur terjadi pada signature, bukan pada niat kita: spawn tidak membaca isi main untuk melihat bahwa join menyusul. Aturannya sederhana — closure yang masuk ke spawn harus mandiri, titik.
Memindahkan Kepemilikan dengan move #
Perbaikannya persis seperti saran kompilator. Kata kunci move mengubah mode penangkapan semua variabel yang dipakai closure menjadi kepemilikan penuh, sehingga closure membawa datanya sendiri dan tuntutan 'static terpenuhi:
use std::thread;
fn main() {
let nomor_pesanan = String::from("PSN-2026-0417");
let pekerja = thread::spawn(move || {
println!("Memproses pesanan {}", nomor_pesanan);
});
pekerja.join().unwrap();
println!("Selesai");
}Output yang Dihasilkan:
Memproses pesanan PSN-2026-0417
SelesaiMari kita bedah kode di atas:
- Baris 6 (
let pekerja = thread::spawn(move || {): satu katamovemengubah segalanya. Saat ekspresi closure dievaluasi, kepemilikannomor_pesananberpindah ke struct anonim closure — data kini ikut ke mana pun closure berpindah, termasuk ke thread baru. - Baris 7 (
println!("Memproses pesanan {}", nomor_pesanan);): di dalam thread,nomor_pesananadalah milik closure, bukan pinjaman darimain. Ketika thread selesai dan closure dijatuhkan,Stringikut di-drop— tidak ada data yang menggantung. - Baris 10 (
pekerja.join().unwrap();):joinmenunggu thread selesai sebelumSelesaitercetak, sehingga urutan dua baris output selalu sama. - Konsekuensi di
main: setelah Baris 6, variabelnomor_pesanandimainsudah tidak bisa dipakai lagi — persis aturan move semantics dari Section Ownership dan Memory. Mencetaknomor_pesanandimainsetelahnya akan memicu E0382 use of moved value.
Untuk tipe yang mengimplementasikan Copy seperti i32, perpindahan ini berjalan lewat penyalinan sehingga variabel asli di main tetap hidup — perilaku yang sudah kita buktikan di Chapter Closure dengan move. Pola ini sangat sering dipakai pada angka-angka kerja: batas stok, nomor meja, atau diskon member cukup tersalin ke setiap thread tanpa upaya apa pun. Kebanyakan data kerja lainnya berupa String, Vec, dan struct yang tidak Copy, jadi anggaplah move ke thread sebagai perpindahan sungguhan yang menuntut perencanaan.
Data untuk Dua Thread: clone Sebelum move #
Satu nilai hanya punya satu pemilik, tetapi sering kali dua thread membutuhkan isi data yang sama — satu thread menghitung karakter laporan, thread lain menghitung barisnya. Karena closure move melahap kepemilikan, data yang sama tidak bisa dipindahkan dua kali. Solusinya: buat salinan mandiri untuk setiap thread sebelum memindahkannya. Urutan pengerjaannya penting — clone() harus terjadi di scope asal, sebelum kedua move dieksekusi:
use std::thread;
fn main() {
let laporan = String::from("Laporan Penjualan Maret\nCabang Jakarta\nCabang Bandung");
let salinan_a = laporan.clone();
let hitung_karakter = thread::spawn(move || salinan_a.chars().count());
let salinan_b = laporan.clone();
let hitung_baris = thread::spawn(move || salinan_b.lines().count());
let jumlah_karakter = hitung_karakter.join().unwrap();
let jumlah_baris = hitung_baris.join().unwrap();
println!("Laporan asli masih utuh di main");
println!("Isinya {} karakter dan {} baris", jumlah_karakter, jumlah_baris);
}Output yang Dihasilkan:
Laporan asli masih utuh di main
Isinya 53 karakter dan 3 barisMari kita bedah kode di atas:
- Baris 6 dan 9 (
let salinan_a = laporan.clone();danlet salinan_b = ...): dua salinanStringdibuat sebelum kedua closure meluncur. Setiap salinan menjadi milik satu thread; tidak ada yang berbagi. - Baris 7 (
thread::spawn(move || salinan_a.chars().count())): closure pendek ini menghitung jumlah karakter dari salinannya sendiri, lalu thread mengembalikan angkanya lewatJoinHandle<usize>. - Baris 12-13: kedua hasil diambil lewat
joindalam urutan penukaran tiket, bukan urutan selesainya thread — kedua worker sebenarnya berjalan bersamaan. - Baris 15 (
println!("Laporan asli masih utuh di main");):laporanmasih sah dipakai dimainkarena yang dipindahkan hanyalah salinan-salinannya. Kedua thread tidak saling mengganggu danmaintidak kehilangan apa pun.
Bila biaya penyalinan dirasa mahal — misalnya laporan berukuran ratusan megabyte — pola clone bukan satu-satunya jalan: channel dapat memindahkan satu kepemilikan ke thread tanpa salinan, dan Arc dapat membagikan satu data ke banyak thread dengan harga satu penunjuk. Kedua senjata itu kita miliki di dua chapter berturut-turut setelah ini.
Ringkasan #
Signature thread::spawn menuntut closure 'static, sehingga pinjaman data dari fungsi pembuat ditolak dengan E0373 — lengkap dengan saran penyisipan move. Kata kunci move memindahkan kepemilikan semua tangkapan ke closure: thread membawa datanya sendiri, variabel asli mati (kecuali tipe Copy), dan program berjalan aman. Dua thread yang membutuhkan data yang sama memakai pola clone sebelum move. Namun perpindahan dan penyalinan hanyalah dua dari tiga cara memindahkan data antar thread — cara ketiga, mengalirkan nilai lewat channel, justru menjadi cara paling idiomatik untuk mengirim pekerjaan ke worker, dan itu topik Chapter berikutnya.