Arc
Anda ingat bahwa kita menggunakan Rc
untuk memberi variabel lebih dari owner. Jika kita melakukan hal yang sama di dalam thread, kita memerlukan Arc
. Arc
singkatan dari "atomic reference counter". Atomic berarti bahwa ia menggunakan prosesor di komputer sehingga datanya hanya ditulis sekali setiap waktu. Ini penting karena jika dua thread menulis data pada waktu yang sama, Anda akan mendapatkan hasil yang salah. Contohnya, bayangkan jika Anda bisa melakukan ini di Rust :
#![allow(unused)] fn main() { // 🚧 let mut x = 10; for i in 0..10 { // Thread 1 x += 1 } for i in 0..10 { // Thread 2 x += 1 } }
Jika Thread 1 dan Thread 2 berjalan bersamaan, mungkin hal seperti ini akan terjadi:
- Thread 1 melihat 10, maka ia menuliskan 11. Kemudian Thread 2 melihat 11, maka ia menulis 12. Tidak ada masalah sejauh ini.
- Thread 1 melihat 12. Pada waktu yang sama, Thread 2 melihat 12. Thread 1 menulis 13. Dan Thread 2 menulis 13. Sekarang kita memili 13, padahal seharusnya adalah 14. Ini adalah masalah yang cukup berbahaya.
Arc
menggunakan prosesor untuk memastikan hal seperti ini tidak terjadi, jadi ini adlaah cara yang harus Anda gunakan di saat Anda menggunakan banyak thread. Anda tidak perlu memakai Arc
jika hanya menggunakan satu thread saja, karena Rc
sedikit lebih cepat dibandingkan dengan Arc
.
Anda tidak bisa mengubah datanya hanya dengan menggunakan Arc
. jadi Anda harus membungkus datanya menggunakan Mutex
, dan membungkus Mutex
dengan Arc
.
Mari kita gunakan Mutex
di dalam Arc
untuk mengubah nilai dari sebuah angka. Pertama-tama, kita gunakn satu thread:
fn main() { let handle = std::thread::spawn(|| { println!("The thread is working!") // melakukan test terhadap thread }); handle.join().unwrap(); // Buat threadnya menunggu sampai semuanya selesai println!("Exiting the program"); }
Sejauh ini, ia akan mencetak:
The thread is working!
Exiting the program
Bagus. Sekarang kita masukkan loop for
dengan iterasi 0..5
:
fn main() { let handle = std::thread::spawn(|| { for _ in 0..5 { println!("The thread is working!") } }); handle.join().unwrap(); println!("Exiting the program"); }
Sejauh ini programnya masih bekerja. Ini adalah hasilnya:
The thread is working!
The thread is working!
The thread is working!
The thread is working!
The thread is working!
Exiting the program
Sekarang, kita buat satu thread lagi. Setiap thread Akan melakukan hal yang sama. Anda bisa melihat bahwa thread bekerja pada waktu yang bersamaan. Terkadang ia akan mengatakan Thread 1 is working!
terlebih dahulu, namun dilain waktu, Thread 2 is working!
yang akan dimunculkan terlebih dahulu. Inilah yang dinamakan sebagai concurrency, yang artinya "running together"/"dijalankan bersama-sama".
fn main() { let thread1 = std::thread::spawn(|| { for _ in 0..5 { println!("Thread 1 is working!") } }); let thread2 = std::thread::spawn(|| { for _ in 0..5 { println!("Thread 2 is working!") } }); thread1.join().unwrap(); thread2.join().unwrap(); println!("Exiting the program"); }
Hasilnya adalah:
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Exiting the program
Sekarang kita ingin mengubah nilai dari my_number
. Typenya adalah i32
. Kita akan menggunakan Arc<Mutex<i32>>
: sebuah type i32
yang bisa diubah, dan diproteksi menggunakan Arc
.
#![allow(unused)] fn main() { // 🚧 let my_number = Arc::new(Mutex::new(0)); }
Sekarang setelah kita memiliki my_number
, kita bisa melakukan clone. Setiap clone bisa menuju ke thread yang berbeda. Kita memiliki dua threads, jadinya kita akan membuat dua clone:
#![allow(unused)] fn main() { // 🚧 let my_number = Arc::new(Mutex::new(0)); let my_number1 = Arc::clone(&my_number); // Clone yang ini akan menuju ke Thread 1 let my_number2 = Arc::clone(&my_number); // Clone yang ini akan menuju ke Thread 2 }
Sekarang kita memiliki safe clone yang melekat ke my_number
, kita bisa melakukan move
(memindahkannya) ke dalam thread yang lain threads tanpa ada masalah apapun.
use std::sync::{Arc, Mutex}; fn main() { let my_number = Arc::new(Mutex::new(0)); let my_number1 = Arc::clone(&my_number); let my_number2 = Arc::clone(&my_number); let thread1 = std::thread::spawn(move || { // Hanya clone yang akan dimasukkan ke Thread 1 for _ in 0..10 { *my_number1.lock().unwrap() +=1; // kunci Mutexnya, ubah nilainya } }); let thread2 = std::thread::spawn(move || { // Hanya clone yang akan dimasukkan ke Thread 2 for _ in 0..10 { *my_number2.lock().unwrap() += 1; } }); thread1.join().unwrap(); thread2.join().unwrap(); println!("Value is: {:?}", my_number); println!("Exiting the program"); }
Hasilnya adalah:
Value is: Mutex { data: 20 }
Exiting the program
Dan, berhasil! :D
Kita bisa menggabungkan dua thread ke dalam sebuah loop for
, dan membuat codenya menjadi lebih singkat.
Kita perlu untuk menyimpan handle (bertype JoinHandles
) sehingga kita bisa menggunakan .join()
untuk setiap handle yang berada di luar loop. Jika kita melakukan ini di dalam loop, ia akan menunggu thread pertama selesai sebelum menjalankan thread yang kedua.
use std::sync::{Arc, Mutex}; fn main() { let my_number = Arc::new(Mutex::new(0)); let mut handle_vec = vec![]; // JoinHandles akan dimasukkan ke sini for _ in 0..2 { // lakukan dua kali let my_number_clone = Arc::clone(&my_number); // Buat clone sebelum memulai thread let handle = std::thread::spawn(move || { // Gunakan clonenya disini for _ in 0..10 { *my_number_clone.lock().unwrap() += 1; } }); handle_vec.push(handle); // simpan handle, sehingga kita bisa menggunakan join pada handle di luar loop // jika kita tidak push handle ke dalam vec, ia akan hangus di sini } handle_vec.into_iter().for_each(|handle| handle.join().unwrap()); // gunakan method join untuk setiap handles println!("{:?}", my_number); }
Dan hasil akhirnya adalah Mutex { data: 20 }
.
Mungkin Arc<Mutex<SomeType>>>
terlihat rumit di Rust, namun ia sangat sering digunakan di Rust, so it becomes natural. Selain itu, Anda selalu dapat menulis code Anda menjadi lebih rapi. Ini adalah code yang menggunakan lebih dari satu statement use
dan dua buah function. Functionnya tidak melakukan sesuatu yang baru. Ia hanya memindahkan beberapa bagian code keluar dari main()
. Anda bisa menuliskan ulang codenya seperti berikut ini apabila Anda merasa code sebelumnya sulit untuk dibaca.
use std::sync::{Arc, Mutex}; use std::thread::spawn; // sehingga kita cukup menuliskan spawn fn make_arc(number: i32) -> Arc<Mutex<i32>> { // Function yang berguna untuk membuat Mutex di dalam Arc Arc::new(Mutex::new(number)) } fn new_clone(input: &Arc<Mutex<i32>>) -> Arc<Mutex<i32>> { // Function untuk membuat arc clone Arc::clone(&input) } // Sekarang main() menjadi lebih mudah untuk dibaca fn main() { let mut handle_vec = vec![]; // setiap handle akan masuk ke sini let my_number = make_arc(0); for _ in 0..2 { let my_number_clone = new_clone(&my_number); let handle = spawn(move || { for _ in 0..10 { let mut value_inside = my_number_clone.lock().unwrap(); *value_inside += 1; } }); handle_vec.push(handle); // handle selesai, sehingga dimasukkan ke dalam vector } handle_vec.into_iter().for_each(|handle| handle.join().unwrap()); // buat setiap handle menunggu println!("{:?}", my_number); }