Lewati ke konten

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.

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
}
▶ Coba sendiri

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.

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 thread
▶ Coba sendiri

Tanpa 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.

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: 5
▶ Coba sendiri

counter.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).

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 = 5050
▶ Coba sendiri

Closure ini tidak memakai variabel dari luar, jadi tidak perlu move. Kedua thread berjalan bersamaan, dan join() menunggu masing-masing thread lalu mengembalikan hasilnya.

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 mampir
thread 2 mampir
thread 3 mampir
▶ Coba sendiri

Coba hapus baris lines.sort() lalu jalankan beberapa kali: urutannya bisa berubah. Sortir itulah yang membuat output-nya selalu sama.

  • thread::spawn(closure) menjalankan closure di thread baru; .join() menunggu thread itu selesai.
  • Closure yang dikirim ke thread biasanya butuh move supaya memiliki penuh data yang dipakainya.
  • Bagikan data antar-thread dengan Arc (ownership bersama) yang digabung dengan Mutex (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

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