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:

rust
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:

text
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:

rust
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:

text
Memproses pesanan PSN-2026-0417
Selesai

Mari kita bedah kode di atas:

  1. Baris 6 (let pekerja = thread::spawn(move || {): satu kata move mengubah segalanya. Saat ekspresi closure dievaluasi, kepemilikan nomor_pesanan berpindah ke struct anonim closure — data kini ikut ke mana pun closure berpindah, termasuk ke thread baru.
  2. Baris 7 (println!("Memproses pesanan {}", nomor_pesanan);): di dalam thread, nomor_pesanan adalah milik closure, bukan pinjaman dari main. Ketika thread selesai dan closure dijatuhkan, String ikut di-drop — tidak ada data yang menggantung.
  3. Baris 10 (pekerja.join().unwrap();): join menunggu thread selesai sebelum Selesai tercetak, sehingga urutan dua baris output selalu sama.
  4. Konsekuensi di main: setelah Baris 6, variabel nomor_pesanan di main sudah tidak bisa dipakai lagi — persis aturan move semantics dari Section Ownership dan Memory. Mencetak nomor_pesanan di main setelahnya 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:

rust
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:

text
Laporan asli masih utuh di main
Isinya 53 karakter dan 3 baris

Mari kita bedah kode di atas:

  1. Baris 6 dan 9 (let salinan_a = laporan.clone(); dan let salinan_b = ...): dua salinan String dibuat sebelum kedua closure meluncur. Setiap salinan menjadi milik satu thread; tidak ada yang berbagi.
  2. Baris 7 (thread::spawn(move || salinan_a.chars().count())): closure pendek ini menghitung jumlah karakter dari salinannya sendiri, lalu thread mengembalikan angkanya lewat JoinHandle<usize>.
  3. Baris 12-13: kedua hasil diambil lewat join dalam urutan penukaran tiket, bukan urutan selesainya thread — kedua worker sebenarnya berjalan bersamaan.
  4. Baris 15 (println!("Laporan asli masih utuh di main");): laporan masih sah dipakai di main karena yang dipindahkan hanyalah salinan-salinannya. Kedua thread tidak saling mengganggu dan main tidak 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.