Rust for PHP Developers (Part 5)
In this part we will talk about some data types which are useful to understand. Some types which come in handy when working with threads and we will also talk about threads. No references to PHP in this article 🙂
Some useful data types
- Cell: –
Cell<T>
is a type that provides interior mutability for types that implement theCopy
trait. - RefCell: –
RefCell<T>
is a type that provides interior mutability for types that do not implement theCopy
trait. It allows you to borrow the value inside it mutably or immutably at runtime (due to which you have to carefully manage the borrow checker).
Let’s look at an example of both types:
use std::cell::{Cell, RefCell}; struct Person { name: String, age: Cell<u32>, friends: RefCell<Vec<String>>, } impl Person { fn new(name: String, age: u32) -> Self { Person { name, age: Cell::new(age), friends: RefCell::new(Vec::new()), } } fn add_friend(&self, friend: String) { self.friends.borrow_mut().push(friend); } fn get_age(&self) -> u32 { self.age.get() } } fn main() { let person = Person::new("Alice".to_string(), 30); person.add_friend("Bob".to_string()); println!("{} is {} years old.", person.name, person.get_age()); println!("Friends: {:?}", person.friends.borrow()); }
In this example, we have a Person
struct that uses Cell
for the age
field and RefCell
for the friends
field. The Cell
type allows us to mutate the age without needing to borrow it, while the RefCell
type allows us to borrow the friends vector mutably at runtime. This has also prevented us from making the Person
struct mutable.
- Mutex: –
Mutex<T>
similar to a cell but it is used for thread safety. It allows you to safely share data between threads by locking the data when it is being accessed. - RwLock: –
RwLock<T>
is a type that provides read-write locks. It allows multiple readers or one writer to access the data at a time. This is useful when you have a lot of read operations and only a few write operations. Again useful for thread safety.
use std::sync::{ Mutex, RwLock}; // for sake of example let's use it without a thread to to understand fn main() { let data = Mutex::new(0); let mut num = data.lock().unwrap(); *num += 1; println!("Mutex Value: {}", num); // !!!! trying to acquire the lock again will cause a deadlock // let mut num = data.lock().unwrap(); let rw_data = RwLock::new(0); let num = rw_data.read().unwrap(); println!("Rw Value: {}", *num); // read again let num2 = rw_data.read().unwrap(); println!("Rw2 Value: {}", *num2); // but the following will not work //let mut num = rw_data.write().unwrap(); // this will cause a deadlock }
Since we are running in main it doesn’t make a lot of sense, however, if this code was running in a thread, it would be useful, we will see that shortly.
Threads
Threads are a way to run multiple tasks concurrently. Rust provides a simple and safe way to create threads using the std::thread
module.
fn main() { for _i in 0..10 { std::thread::spawn(|| { println!("I am in a thread "); }); } }
This code creates 10 threads that print a message. The std::thread::spawn
function takes a closure as an argument and creates a new thread to run it. However, the main thread might finish before the spawned threads, so you might not see all the messages printed. To ensure that the main thread waits for all spawned threads to finish, you can use join
:
use std::thread; fn main() { let mut handles = vec![]; for _i in 0..10 { let handle = thread::spawn(|| { println!("I am in a thread "); }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } }
Imagine that instead of doing that simple closure you wanted to move some data around, update certain value in the closure. If what you are passing implements Copy
like the integer below, you will be able to compile but it won’t work as expected i.e. the counter value will not be incremented by 10. If passed value doesn’t impment Copy
, the data is moved into the closure and you cannot access it anymore in another thread so won’t compile.
use std::thread; fn main() { let mut handles = vec![]; let mut data = 0; for _i in 0..10 { let handle = thread::spawn(move || { println!("I am in a thread value {}", data); data += 1; // this will not work as expected }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } }
This is where `Arc`comes in, which has an equivalent in the non-threading world, Rc
. For now let’s just talk about Arc
to understand both. Imaging a scenario where you want to share data between threads. You can use Arc
(Atomic Reference Counted) to safely share data between threads.
use std::sync::{Arc, Mutex}; use std::{thread, time::Duration}; fn main() { let shared = Arc::new(Mutex::new(0)); let mut handles = vec![]; for i in 0..10 { let shared = Arc::clone(&shared); let handle = thread::spawn(move || { println!("Thread {} wants the lock", i); let mut num = shared.lock().unwrap(); println!("Thread {} acquired the lock", i); *num += 1; thread::sleep(Duration::from_millis(200)); // simulate work println!("Thread {} releasing lock", i); }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Final value: {}", *shared.lock().unwrap()); }
In this example, we create an Arc<Mutex<i32>>
to share an integer between multiple threads. Each thread locks the mutex, increments the value, and then releases the lock. You can see how other threads are waiting and can access that data when available without having to worry about the borrow checker.
Conclusion
In this part, we covered some useful data types in Rust, including Cell
, RefCell
, Mutex
, and RwLock
. We also discussed how to create and manage threads in Rust using the std::thread
module and how to safely share data between threads using Arc
. Understanding these concepts is essential for writing concurrent and parallel programs in Rust. In the next part, we will cover some more advanced topics, including error handling and testing.