Masai Mahapa

30 days of Rust - Day Eighteen - Concurrency

30 days of rust - day Eighteen Modern day computers are very fast at processing data. This is partly thanks to concurrent programming as well as parallel programming. These two methods have been known to be likely to generate a lot of bugs, of which Rust's team is working hard to change.

Concurrent programming refers to different parts of a program executed independently.

Parallel programming refers to different parts of a program executed at the same time.

Rust refers to both of these as just concurrency to keep things simple.

Day 18 - Concurrency

A single processor on your computer can process multiple tasks so fast that it can seem as if they're all happening at the same time. What really happens under the hood is a processor multi-tasks switching from one task to the other the same way you can be in the kitch washing the dishes while cooking at the same time. Just a whole lot faster.

Splitting computation this way greatly improves performance, with added complexity though. The challenge is having those tasks run in order.

  • Race condition : threads are accessing data in an inconsistent order.
  • Deadlocks : when two threads are waiting for each other.
  • Bugs which are hard to reproduce.

Using multiple threads in rust

In order to create new threads, we use the thread::spawn and pass it a closure of the code we want it to run on that newly created thread.

use std::thread;
use std::time::Duration;
 
fn main() {
   thread::spawn(|| {
       for i in 1..10 {
           println!("hi number {} from the spawned thread!", i);
           thread::sleep(Duration::from_millis(1));
       }
   });
 
   for i in 1..5 {
       println!("hi number {} from the main thread!", i);
       thread::sleep(Duration::from_millis(1));
   }
}

So the above code creates a main thread when it is run. Inside the main thread, we call thread::spawn which creates a new thread running the code inside it. Then thread::sleep basically makes the thread pause for a bit. Basically while it is paused, the processor will switch to the other thread in the meantime. The output of the above code is;

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!

Wait a minute. Isn't the spawned thread supposed to run all the way from 1 to 9? Well, yes that's what it's supposed to do but the issue is that once the main thread has completed its job, the entire program shuts down. It doesn't care what else still needs to be performed.

Luckily, there is a way to solve this. The thread::spawn method returns a JoinHandle<()> which we can call a .join method on which will pause the main thread from continuing any further or exiting until the child thread has completed its job.

Let's update our code;

use std::thread;
use std::time::Duration;
 
fn main() {
   let handle = thread::spawn(|| {
       for i in 1..10 {
           println!("hi number {} from the spawned thread!", i);
           thread::sleep(Duration::from_millis(1));
       }
   });
 
   for i in 1..5 {
       println!("hi number {} from the main thread!", i);
       thread::sleep(Duration::from_millis(1));
   }
 
   handle.join().unwrap();
}
 

The output will now look like;

hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!

Moving ownership between threads

In order to use variables from the main thread inside the closure, we need to use the move keyword. What this does is it moves the ownership from the main thread to the child thread.

use std::thread;
 
fn main() {
   let v = vec![1, 2, 3];
 
   let handle = thread::spawn(move || {
       println!("Here's a vector: {:?}", v);
   });
 
   handle.join().unwrap();
}

So basically, Rust's ownership saved us from having to introduce potential bugs. In the code above, once we use the move keyword, we are basically getting a guarantee that the main thread will never use the v variable again. This is because the new thread might change or even drop v which could cause issues if the main thread tries using it again.

Transferring data between threads using channels

In order to achieve safe concurrency, Rust has message passing which is an idea apparently popularized by the Go programming language. This is implemented by Channels.

A nice way to think of channels is by the analogy of a river. If you put a rubber duck in a river, it will flow downstream.

A channel has 2 parts, namely a transmitter and receiver. The transmitter puts the rubber duck and the receiver catches it downstream.

use std::sync::mpsc;
use std::thread;
fn main() {
   // create a channel
   let (sender, receiver) = mpsc::channel();
   // create a new thread, create a value, send the value using the channel to main thread
   thread::spawn(move || {
       let val = String::from("I was created in the child thread, will be sent to main thread");
       sender.send(val).unwrap();
   });
   // receive and print the message from the child thread
   let received = receiver.recv().unwrap();
   println!("I have received this message from the child thread: {}", received);
}

The code above outputs;

I have received this message from the child thread: I was created in the child thread, will be sent to main thread

Basically what happened is that the main thread created a channel, then thereafter created a child thread and passed to it the sender. The child thread sends a message "val" downstream which is later received by the main thread.

So imagine if our code has to do some expensive calculations. We could create a new thread and have it work on the calculations and return a result later without blocking the execution of the main thread. Nice.

Rust also allows for sharing state concurrently, which is not as highly recommended and a bit more complex than using channels. You can visit the docs to learn more about it here

Conclusion

Rust has amazing type and ownership checks which help solve the concurrency issue in a relatively safe and easier way. This helps us avoid many hard to resolve bugs known in many other programming languages when it comes to concurrency.

Rust actually titled this fearless concurrency.

Thanks for spending some time learning Rust with me. Until next time. Peace ✌🏼 .

Share