Rust allows us to create threads to run codes concurrently and work with data – access, return and modify using the move keyword.
Create Threads
We can create threads using the spawn function from the thread module. The function accepts a closure with no arguments. The closure can work with data. Consider the following codes that run a single thread. The thread only prints out a line of text 10 times before the main function completes.
1 2 3 4 5 6 7 8 9 10 11 12 | use std::thread; fn main() { let join_handle = thread::spawn(|| { for i in 1..10 { println!("Thread {} from the spawned!", i); } }); // Ensure the thread finishes execution before the main function does join_handle.join().unwrap(); } |
Sample output from our lone thread.
1 2 3 4 5 6 7 8 9 | Thread 1 from the spawned! Thread 2 from the spawned! Thread 3 from the spawned! Thread 4 from the spawned! Thread 5 from the spawned! Thread 6 from the spawned! Thread 7 from the spawned! Thread 8 from the spawned! Thread 9 from the spawned! |
We can create a number of threads and force the main function to wait until the threads finish. Consider the following codes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | use std::thread; fn main() { let join_handle_a = thread::spawn(|| { for i in 1..10 { println!("ThreadA {} from the spawned!", i); } }); let join_handle_b = thread::spawn(|| { for i in 1..10 { println!("ThreadB {} from the spawned!", i); } }); // Ensure the threads finish execution before the main function does join_handle_a.join().unwrap(); join_handle_b.join().unwrap(); } |
Sample output from our threads. Note that the same result is not guaranteed after every re-run.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ThreadA 1 from the spawned! ThreadA 2 from the spawned! ThreadA 3 from the spawned! ThreadA 4 from the spawned! ThreadA 5 from the spawned! ThreadA 6 from the spawned! ThreadA 7 from the spawned! ThreadB 1 from the spawned! ThreadB 2 from the spawned! ThreadB 3 from the spawned! ThreadB 4 from the spawned! ThreadB 5 from the spawned! ThreadB 6 from the spawned! ThreadB 7 from the spawned! ThreadB 8 from the spawned! ThreadB 9 from the spawned! ThreadA 8 from the spawned! ThreadA 9 from the spawned! |
If we have a lot of threads running, it’s difficult to identify which thread does what operation. We could name them! The code snippet below names a thread “Process 001”, runs it, and displays its name.
1 2 3 4 5 6 7 8 9 10 | use std::thread; fn main() { let process_001 = thread::Builder::new().name("Process 001".to_string()).spawn(move || { let handle = thread::current(); println!("Executing {}", handle.name().unwrap()); }).unwrap(); process_001.join().unwrap(); } |
The codes output the following.
1 | Executing Process 001 |
Threads That Work With Data
If spawn accepts a closure which accepts 0 arguments, how do we pass data to and from a thread? We use variables created in another block inside the closure. Consider the following codes. We have 2 threads each using the display_vec function and a Vec instance. Notice that the Vec instances are declared and initialized outside of the closures but within the main function. Note that we need to use the move keyword with the closures.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | use std::thread; use std::sync::Arc; fn display_vec(vec: Vec<i32>) { println!("Vec in Thread contains {:?} ", vec); } fn main() { let mut v1:Vec<i32> = Vec::new(); v1.push(4); v1.push(5); v1.push(100); let mut v2:Vec<i32> = Vec::new(); v2.push(40); v2.push(50); v2.push(500); let arc_data_for_v1 = Arc::new(v1); let arc_data_for_v1_for_thread = arc_data_for_v1.clone(); let arc_data_for_v2 = Arc::new(v2); let arc_data_for_v2_for_thread = arc_data_for_v2.clone(); let join_handle_with_input_a = thread::spawn(move || display_vec(arc_data_for_v1_for_thread.to_vec())); let join_handle_with_input_b = thread::spawn(move || display_vec(arc_data_for_v2_for_thread.to_vec())); join_handle_with_input_a.join().unwrap(); join_handle_with_input_b.join().unwrap(); println!("{:?}", arc_data_for_v1); println!("{:?}", arc_data_for_v2); } |
This outputs
1 2 3 4 | Vec in Thread contains [4, 5, 100] Vec in Thread contains [40, 50, 500] [4, 5, 100] [40, 50, 500] |
Output
1 2 | Vec in Thread contains [4, 5, 100] Vec in Thread contains [40, 50, 500] |
To return values from threads, we pass closures that return data values. The code snippet below has a closure that returns 100.
1 2 3 4 5 6 | use std::thread; fn main() { let join_handle_with_return = thread::spawn(|| 110); println!("{}", join_handle_with_return.join().unwrap()); } |
We can also have threads that modify the values of its parameters and reflect the changes back to the calling codes. Check the codes below. In this case, we would want to modify a struct instance in a separate thread but display the same instance in the main thread.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | use std::thread; use std::sync::Arc; use std::sync::Mutex; use std::fmt::{Display, Formatter, Error}; struct Person { first_name: String, last_name: String } impl Display for Person { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { writeln!(f, "{} {}", self.last_name, self.first_name) } } fn main() { let arc_data = Arc::new(Mutex::new(Person{ first_name: "karl".to_string(), last_name: "sg".to_string() })); let thread_data = arc_data.clone(); let join_handle_with_borrow = thread::spawn(move || { let mut d = thread_data.lock().unwrap(); d.last_name = "karl2".to_string(); d.first_name = "sg2".to_string(); }); join_handle_with_borrow.join().unwrap(); println!("{}", arc_data.lock().unwrap()); } |
This outputs
1 | karl2 sg2 |
Tested using Rust 1.40.0.