Multiple threads
Jika kita menggunakan multiple threads, Anda bisa melakukan banyak hal dalam waktu yang bersamaan. Komputer modern memiliki lebih dari satu core sehingga mereka bisa melakukan lebih dari satu task pada waktu yang bersamaan, dan Rust memungkinkan Anda untuk menggunakannya. Rust menggunakan threads yang biasa disebut dengan "OS threads". OS thread artinya bahwa operating system membuat threadnya pada core yang berbeda. (Beberapa bahasa pemrograman lainnya menggunakan "green threads", yang mana itu kurang begitu powerful)
Kita membuat threadnya menggunakan std::thread::spawn
dan kemudian menuliskan closure untuk memberitahu compiler tentang apa yang dilakukan oleh thread tersebut. Threads sangatlah menarik karena mereka berjalan pada waktu yang sama, dan Anda bisa mencobanya untuk melihat apa yang sebenarnya terjadi. Ini adalah contoh sederhananya:
fn main() { std::thread::spawn(|| { println!("I am printing something"); }); }
Jika Anda menjalankan code di atas, ia akan berbeda-beda setiap saat. Terkadang ia mencetak hasilnya, dan terkadang juga tidak mencetak apapun (ini tergantung pada kecepatan komputer Anda). Ini karena terkadang main()
selesai sebelum threadnya selesai. Dan jika main()
selesai, tentunya program berakhir. Akan lebih mudah melihatnya apagila kita meletakkan threadnya di dalam for
loop:
fn main() { for _ in 0..10 { // buat sebanyak 10 threads std::thread::spawn(|| { println!("I am printing something"); }); } // Sekarang threadnya mulai berjalan. } // Berapa banyak thread yang selesai dijalankan sebelum main() berakhir disini?
Biasanya ada 4 thread yang melakukan print sebelum main
berakhir, namun hasil ini akan selalu berbeda. Jika komputer Anda lebih cepat, maka mungkin thread tidak akan mencetak apapun. Juga, terkadang threadnya akan panic:
thread 'thread 'I am printing something
thread '<unnamed><unnamed>thread '' panicked at '<unnamed>I am printing something
' panicked at 'thread '<unnamed>cannot access stdout during shutdown' panicked at '<unnamed>thread 'cannot access stdout during
shutdown
Ini adalah error saat thread mencoba untuk melakukan sesuatu tepat di saat program dimatikan.
Anda bisa memberikan sesuatu kepada komputer agar programnya tidak langsung mati. Setidaknya kita membuat komputer menjadi sibuk, sehingga thread bisa menyelesaikan tugasnya:
fn main() { for _ in 0..10 { std::thread::spawn(|| { println!("I am printing something"); }); } for _ in 0..1_000_000 { // buat program mendeklarasikan "let x = 9" sebanyak 1 juta kali // Program harus menyelesaikan ini sebelum akhirnya ia bisa keluar dari function main() let _x = 9; } }
Tetapi, cara di atas itu adalah cara yang konyol silly way untuk memberikan waktu kepada threads untuk menyelesaikan task-tasknya. Cara yang agak lebih baik adalah mem-binding threads tersebut ke sebuah variable. Jika Anda menambahkan let
, maka Anda akan membuat sesuatu yang bertype JoinHandle
. Anda bisa melihat ini di dalam signature dari method spawn
:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
(f
adalah closure - Nantinya kita akan mempelajari bagaimana untuk meletakkan closures ke dalam function yang kita buat)
Sehingga sekarang kita memiliki JoinHandle
setiap saat.
fn main() { for _ in 0..10 { let handle = std::thread::spawn(|| { println!("I am printing something"); }); } }
handle
sekarang bertype JoinHandle
. Apa yang akan kita lakukan dengan JoinHandle
? Kita akan menggunakan method yang bernama .join()
. Method ini berarti "tunggu sampai semua threads selesai" (ia akan menunggu thread untuk bergabung). Jadi sekarang kita hanya cukup menuliskan handle.join()
dan ia akan menunggu setiap thread selesai dikerjakan.
fn main() { for _ in 0..10 { let handle = std::thread::spawn(|| { println!("I am printing something"); }); handle.join(); // tunggu semua thread selesai dikerjakan } }
Sekarang kita akan mempelajari tentang 3 type dari closure. Tiga type tersebut adalah:
FnOnce
: mengambil valuenyaFnMut
: mengambil mutable referenceFn
: mengambil regular reference
Closure akan mencoba untuk mengambil Fn
jika ia bisa. Tapi, jika ia perlu untuk mengganti valuenya, ia akan menggunakan FnMut
, dan jika ia perlu untuk mengambil valuenya sepenuhnya, ia akan menggunakan FnOnce
. FnOnce
adalah nama yang bagus karena ia menjelaskan apa yang ia lakukan: ia mengambil valuenya sekali, dan kemudian tidak akan (dan juga tidak bisa) mengambilnya lagi.
Ini adalah contohnya:
fn main() { let my_string = String::from("I will go into the closure"); let my_closure = || println!("{}", my_string); my_closure(); my_closure(); }
String
bukanlan Copy
, sehingga my_closure()
adalah Fn
: ia mengambil referencenya.
Jika melakukan perubahan pada my_string
, ia akan menjadi FnMut
.
fn main() { let mut my_string = String::from("I will go into the closure"); let mut my_closure = || { my_string.push_str(" now"); println!("{}", my_string); }; my_closure(); my_closure(); }
Hasil cetaknya adalah:
I will go into the closure now
I will go into the closure now now
Dan jika Anda mengambil valuenya, maka ia akan menjadi FnOnce
.
fn main() { let my_vec: Vec<i32> = vec![8, 9, 10]; let my_closure = || { my_vec .into_iter() // into_iter mengambil ownership .map(|x| x as u8) // ubah setiap elementnya menjadi u8 .map(|x| x * 2) // kalikan dengan 2 .collect::<Vec<u8>>() // taruh semua hasilnya ke dalam Vec }; let new_vec = my_closure(); println!("{:?}", new_vec); }
Kita mengambil valuenya, sehingga kita tidak bisa menjalankan my_closure()
lebih dari sekali. Dari sana nama FnOnce
berasal.
Jadi, sekarang kita kembali ke threads. Mari kita coba untuk menggunakan sebuah value dari luar thread:
fn main() { let my_string = String::from("Can I go inside the thread?"); let handle = std::thread::spawn(|| { println!("{}", my_string); // ⚠️ }); handle.join().unwrap(); }
Compiler akan mengatakan bahwa ini tidak bekerja.
error[E0373]: closure may outlive the current function, but it borrows `my_string`, which is owned by the current function
--> src\main.rs:28:37
|
28 | let handle = std::thread::spawn(|| {
| ^^ may outlive borrowed value `my_string`
29 | println!("{}", my_string);
| --------- `my_string` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src\main.rs:28:18
|
28 | let handle = std::thread::spawn(|| {
| __________________^
29 | | println!("{}", my_string);
30 | | });
| |______^
help: to force the closure to take ownership of `my_string` (and any other referenced variables), use the `move` keyword
|
28 | let handle = std::thread::spawn(move || {
| ^^^^^^^
Ini merupakan pesan error yang panjang, tetapi sangat membantu: ia menyarankan untuk use the `move` keyword
(menggunakan keyword move
). Masalahnya adalah kita bisa melakukan apapun terhadap my_string
di saat thread sedang menggunakannya, namun tidak memiliki kepemilikan atas value tersebut. Hal seperti tentulah samas sekali tidak aman.
Mari kita, lagi-lagi, membuat program yang tidak bisa dijalankan :D :
fn main() { let mut my_string = String::from("Can I go inside the thread?"); let handle = std::thread::spawn(|| { println!("{}", my_string); // sekarang my_string digunakan sebagai reference }); std::mem::drop(my_string); // ⚠️ Kita mencoba untuk melakukan drop pada my_string disini. Namun threadnya masih memerlukan my_string. handle.join(); }
Sehingga Anda perlu untuk mengambil valuenya menggunakan move
. Dan sekarang programnya safe:
fn main() { let mut my_string = String::from("Can I go inside the thread?"); let handle = std::thread::spawn(move|| { println!("{}", my_string); }); std::mem::drop(my_string); // ⚠️ kita tidak bisa melakukan drop disini, karena variabel handle memilikinya. Jadi ini tidak akan berjalan handle.join(); }
Jadinya kita hapuskan std::mem::drop
, dan sekarang programnya berjalan. handle
mengambil my_string
dan code kita menjadi safe.
fn main() { let mut my_string = String::from("Can I go inside the thread?"); let handle = std::thread::spawn(move|| { println!("{}", my_string); }); handle.join(); }
Jadi cukup diingat: jika Anda memiliki value di dalam thread yang berasal dari luar thread, Anda perlu menggunakan move
.