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

  1. Cell: – Cell<T> is a type that provides interior mutability for types that implement the Copy trait.
  2. RefCell: – RefCell<T> is a type that provides interior mutability for types that do not implement the Copy 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.

  1. 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.
  2. 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 CellRefCellMutex, 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.

Leave a Reply

Your email address will not be published. Required fields are marked *