Konkurensi di Rust
Konkurensi (concurrency), yaitu menjalankan beberapa pekerjaan sekaligus, adalah bagian yang benar-benar berbahaya di banyak bahasa pemrograman. Dua thread (alur eksekusi) yang menyentuh data yang sama di waktu yang sama bisa merusak data itu, dan bug seperti ini sangat sulit dilacak. Justru bug jenis inilah yang ingin dicegah oleh aturan borrowing (peminjaman), dan perlindungan itu berlaku langsung juga untuk thread.
Membuat thread
Section titled “Membuat thread”use std::thread;
fn main() { let handle = thread::spawn(|| { for i in 1..=3 { println!("thread: {i}"); } });
for i in 1..=3 { println!("main: {i}"); }
handle.join().unwrap(); // tunggu sampai thread selesai}thread::spawn menerima sebuah closure dan menjalankannya di thread baru, berbarengan dengan kode lainnya. .join() menunggu (memblokir) sampai thread itu selesai. Tanpa .join(), main bisa saja selesai duluan sebelum thread yang baru dibuat sempat berjalan.
Memindahkan data ke dalam thread
Section titled “Memindahkan data ke dalam thread”Closure yang dikirim ke thread::spawn harus memiliki penuh semua data yang dipakainya, karena closure itu mungkin baru berjalan setelah scope asalnya selesai. Tambahkan kata kunci move untuk memaksanya:
use std::thread;
fn main() { let message = String::from("halo dari sebuah thread");
let handle = thread::spawn(move || { println!("{message}"); });
handle.join().unwrap();}Output:
halo dari sebuah threadTanpa move, Rust tidak mau meng-compile kode ini. Rust tidak bisa menjamin message masih valid saat thread baru benar-benar menjalankannya. move memindahkan ownership message ke dalam closure. Ini “move” yang sama dengan yang kamu lihat di halaman Ownership, hanya saja kali ini diserahkan ke thread, bukan ke variabel lain.
Berbagi data antar-thread dengan aman
Section titled “Berbagi data antar-thread dengan aman”Referensi biasa tidak bisa dipakai lintas thread (referensi tidak boleh hidup lebih lama dari scope yang membuatnya). Jadi, untuk berbagi data antar-thread, kamu menggabungkan dua hal yang sudah kamu kenal: Arc (dari halaman Smart Pointer) untuk ownership bersama, dan Mutex untuk menjamin hanya satu thread yang menyentuh data pada satu waktu:
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![];
for _ in 0..5 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); }
for handle in handles { handle.join().unwrap(); }
println!("Hasil: {}", *counter.lock().unwrap());}Output:
Hasil: 5counter.lock() menunggu sampai ia bisa mendapat akses eksklusif ke nilai itu dengan aman, lalu kamu melakukan perubahan, dan kuncinya dilepas otomatis. Lima thread menambah penghitung yang sama, dan berkat Mutex, mereka tidak pernah saling menimpa. Hasilnya selalu 5, bukan angka yang lebih kecil karena ada perubahan yang hilang.
Mengunci data bersama adalah salah satu cara thread bekerja sama. Cara umum lainnya adalah tidak berbagi sama sekali, dan membiarkan thread saling mengirim pesan. Itulah isi halaman Channel.
Kenapa Rust menyebutnya “fearless concurrency”
Section titled “Kenapa Rust menyebutnya “fearless concurrency””Di kebanyakan bahasa, tidak ada yang mencegahmu lupa mengunci Mutex, atau tidak sengaja membagikan referensi ke thread lain padahal seharusnya tidak. Compiler Rust benar-benar memeriksa hal ini: kode yang bisa menyebabkan data race (dua thread mengakses data yang sama tanpa koordinasi) tidak akan bisa di-compile. Ini sistem ownership dan borrowing yang sama yang sudah kamu pelajari sebelumnya di kursus ini, hanya diterapkan pada thread, bukan pada satu scope saja. Karena itu konkurensi di Rust disebut “fearless” (tanpa rasa takut).
Latihan
Section titled “Latihan”1. Dua thread, satu jumlah
Section titled “1. Dua thread, satu jumlah”Jumlahkan angka 1 sampai 100 memakai dua thread: satu menjumlahkan 1 sampai 50, yang lain 51 sampai 100. Lalu main menjumlahkan kedua hasilnya. Petunjuk: closure milik thread bisa mengembalikan nilai, dan handle.join().unwrap() memberikan nilai itu kembali kepadamu.
Lihat solusi
use std::thread;
fn main() { let first = thread::spawn(|| { let total: u32 = (1..=50).sum(); total }); let second = thread::spawn(|| { let total: u32 = (51..=100).sum(); total });
let a = first.join().unwrap(); let b = second.join().unwrap(); println!("{a} + {b} = {}", a + b);}Output:
1275 + 3775 = 5050Closure ini tidak memakai variabel dari luar, jadi tidak perlu move. Kedua thread berjalan bersamaan, dan join() menunggu masing-masing thread lalu mengembalikan hasilnya.
2. Semua menulis di buku tamu
Section titled “2. Semua menulis di buku tamu”Jalankan tiga thread. Masing-masing menambahkan satu baris seperti "thread 2 mampir" ke satu Vec<String> bersama. Setelah semua thread selesai, cetak baris-baris itu dalam urutan yang sudah disortir. Petunjuk: pakai Arc<Mutex<Vec<String>>> seperti contoh penghitung, dan sortir salinannya di akhir, karena thread bisa selesai dalam urutan apa saja.
Lihat solusi
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let book = Arc::new(Mutex::new(Vec::new())); let mut handles = vec![];
for id in 1..=3 { let book = Arc::clone(&book); handles.push(thread::spawn(move || { book.lock().unwrap().push(format!("thread {id} mampir")); })); }
for handle in handles { handle.join().unwrap(); }
let mut lines = book.lock().unwrap().clone(); lines.sort(); for line in lines { println!("{line}"); }}Output:
thread 1 mampirthread 2 mampirthread 3 mampirCoba hapus baris lines.sort() lalu jalankan beberapa kali: urutannya bisa berubah. Sortir itulah yang membuat output-nya selalu sama.
Ringkasan
Section titled “Ringkasan”thread::spawn(closure)menjalankan closure di thread baru;.join()menunggu thread itu selesai.- Closure yang dikirim ke thread biasanya butuh
movesupaya memiliki penuh data yang dipakainya. - Bagikan data antar-thread dengan
Arc(ownership bersama) yang digabung denganMutex(akses eksklusif).counter.lock()memberi akses yang aman, satu thread pada satu waktu. - Compiler menolak kode yang bisa menyebabkan data race saat compile, bukan saat program berjalan.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Kenapa closure yang dikirim ke thread::spawn biasanya butuh kata kunci move?
2. Untuk apa Arc<Mutex<T>> dipakai?
3. Apa yang bisa terjadi kalau kamu lupa memanggil .join() pada handle sebuah thread?
4. Kenapa urutan baris "main:" dan "thread:" bisa berbeda setiap kali program yang sama dijalankan?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- Using Threads to Run Code Simultaneously — The Rust Book (bahasa Inggris):
spawn,joindan closuremove, dengan contoh bagaimana output thread bisa berselang-seling. - Shared-State Concurrency — The Rust Book (bahasa Inggris):
MutexdanArcsecara mendalam, termasuk kenapaRctidak bisa dikirim ke thread lain. std::thread— dokumentasi standard library (bahasa Inggris): semua tentang thread, termasuk memberi nama thread dan scoped thread.Mutex— dokumentasi standard library (bahasa Inggris): cara kerja penguncian dan apa arti “poisoning” saat sebuah thread panic sambil memegang kunci.- Threads — Rust by Example (bahasa Inggris): contoh singkat yang bisa dijalankan, yang membuat dan menunggu beberapa thread.