🚀
18 Concurrency

Concurrency

  • Concurrency programming is different parts of your program executes independently and may be running at the same time.

  • Parallelism is when the program is running at the same time.

  • Concurrent, indoor parallel by leveraging system and powerful ownership. A lot of concurrency errors are able to be caught at compile-time in rust.

In most current operating system and executive programs code is within a process, and the operating system manages running multiple processes at once. Within a program you could have independent parts that run simultaneously features that run theses independent parts are called threads.

Splitting your programs computation into multiple threads can improve performance multiple parts of your program are running at the same time x

But this also increases complexity because threads run simultaneously you don't have control over the order in which different parts of your program or executed, which leads to some unique challenges, such as

  • Race conditions
  • Threads (accessing data or resources in an inconsistent state)
  • Deadlocks (where we have two threads that are both waiting for a resource that the other thread has this making both threads wait indefinitely)

Because execution order is nondeterministic can appear that only happened in certain situations in our heart to reproduce and fix reliably.

Rust attempts to mitigate the negative effects of using threats however it's on the programmer to be thoughtful when designing their program for a multithreaded context.

Two main types of threads threads:

  1. OS threads
    • also known as native threads, system threads, etc. are managed by the operating system.
  2. Green threads
    • also known as user threads or program threads, etc. are managed by the runtime library or virtual machine.

Many operating provide an API for creating threads

  • One-to-one Threads (1-1 model)

    • Directly maps to an Operating system thread
  • Many-to-one Threads (M-N model)

    • Green threads don't have a one-to-one mapping to OS threads
    • Example: 20 green threads map to 5 OS threads

Each Programming Language has its own threading model, and the threading model is a part of the language runtime.

Rust aims to have a minimal runtime, trade-off in features, because Green threads require a larger runtime only includes one-to-one threads or OS threads in it standard library, Rust does not include green threads in its standard library.

use std::thread;
 
fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("Spawn Thread: {}", i);
            thread::sleep(std::time::Duration::from_millis(1));
        }
    });
 
    for i in 1..5 {
        println!("Main Thread: {}", i);
        thread::sleep(std::time::Duration::from_millis(1));
    }
}
 
$ cargo run
 
Main  Thread: 1
Spawn Thread: 1
Main  Thread: 2
Spawn Thread: 2
Main  Thread: 3
Spawn Thread: 3
Main  Thread: 4
Spawn Thread: 4
Spawn Thread: 5

Execution order is nondeterministic, also noticed that the main thread finish printing all of it numbers ranged from 1 to 5, so we printed 1 to 4.

But the thread didn't finish printing all of it numbers the range was 1 to 10, but we only got to number 5.

Because when the main thread ends, the spawn thread is stopped no matter if it finished executing or not.

Allows spawn thread to finish execution

  • Join handles are used to wait for a thread to finish executing before the main thread continues.

  • Calling .join() will block the main thread until the spawned thread has finished executing.

use std::thread;
 
fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("Spawn Thread: {}", i);
            thread::sleep(std::time::Duration::from_millis(1));
        }
    });
 
    for i in 1..5 {
        println!("Main  Thread: {}", i);
        thread::sleep(std::time::Duration::from_millis(1));
    }
 
    handle.join().unwrap();
}
$ cargo run
 
Main  Thread: 1
Spawn Thread: 1
Main  Thread: 2
Spawn Thread: 2
Main  Thread: 3
Spawn Thread: 3
Main  Thread: 4
Spawn Thread: 4
Spawn Thread: 5
Spawn Thread: 6
Spawn Thread: 7
Spawn Thread: 8
Spawn Thread: 9

Allowing spawn thread to finish before main thread starts

  • The main thread can wait for the spawned thread to finish before it starts executing.
  • This is done by calling .join() on the thread handle before the main thread starts executing.
use std::thread;
 
fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("Spawn Thread: {}", i);
            thread::sleep(std::time::Duration::from_millis(1));
        }
    });
 
    handle.join().unwrap();
 
    for i in 1..5 {
        println!("Main  Thread: {}", i);
        thread::sleep(std::time::Duration::from_millis(1));
    }
}
 
$ cargo run
 
Spawn Thread: 1
Spawn Thread: 2
Spawn Thread: 3
Spawn Thread: 4
Spawn Thread: 5
Spawn Thread: 6
Spawn Thread: 7
Spawn Thread: 8
Spawn Thread: 9
Main  Thread: 1
Main  Thread: 2
Main  Thread: 3
Main  Thread: 4

Move Closures in Threads

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

Error: closure may outlive the current function, but it borrows v, which is owned by the current function

$ cargo run
 
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
  --> src/main.rs:24:32
   |
24 |     let handle = thread::spawn(|| {
   |                                ^^ may outlive borrowed value `v`
25 |         println!("Here's a vector: {:?}", v);
   |                                           - `v` is borrowed here
   |
 
  • Rust does not how long the spawned thread will run for, so it doesn't know if the vector v will still be valid when the spawned thread tries to access it.

  • We use the move keyword to tell Rust to move the ownership of v into the spawned thread.

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

Transferring Data between Threads

  • One increasingly popular approach to ensure safe concurrency is message passing

  • Where you have threads / Actors passing messages to each other which contain data, The GO programming Language has a popular slogan

"Do not communicate by sharing memory; instead, share memory by communicating"

One tool that Rust provides for message passing is channels, which are a way to send data between threads, which is included in the standard library.

  • Channels are a way to send data between threads, which is included in the standard library.

  • The Channel is closed when one half of the channel is dropped.

Using Single Produce

MPSC

  • Multiple Producers, Single Consumer
use std::sync::mpsc;
use std::thread;
 
fn main() {
    let (tx, rx) = mpsc::channel();
 
    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });
 
    let received = rx.recv().unwrap();
    println!("Got: {}", received);
}
$ cargo run
 
Got: hi
  • The mpsc::channel() function returns a tuple containing a transmitter and a receiver.
  • The transmitter is used to send data to the channel, and the receiver is used to receive data from the channel.
  • The tx.send(val).unwrap() function sends the value val to the channel.
  • The rx.recv().unwrap() function receives the value from the channel.
  • The unwrap() function is used to handle any errors that may occur when sending or receiving data from the channel.
  • The move keyword is used to move the ownership of the value val into the spawned thread.
use std::sync::mpsc;
use std::thread;
 
fn main() {
    let (tx, rx) = mpsc::channel();
 
    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread")
        ];
 
        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(std::time::Duration::from_secs(1));
        }
    });
 
    for received in rx {
        println!("Got: {}", received);
    }
}
 
$ cargo run
 
Got: hi
Got: from
Got: the
Got: thread

Creating Multiple Produces

use std::sync::mpsc;
use std::thread;
 
fn main() {
    let (tx, rx) = mpsc::channel();
 
    let tx2 = tx.clone();
 
    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread")
        ];
 
        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(std::time::Duration::from_secs(1));
        }
    });
 
    thread::spawn(move || {
        let vals = vec![
            String::from("more"),
            String::from("messages"),
            String::from("for"),
            String::from("you")
        ];
 
        for val in vals {
            tx2.send(val).unwrap();
            thread::sleep(std::time::Duration::from_secs(1));
        }
    });
 
    for received in rx {
        println!("Got: {}", received);
    }
}
$ cargo run
 
Got: hi
Got: more
Got: from
Got: messages
Got: the
Got: for
Got: thread
Got: you

Shared State Concurrency

  • Shared state concurrency is when multiple threads access the same data or resources at the same time.

  • Mutex is a mutual exclusion primitive that provides shared access to data.

  • Locks are used to ensure that only one thread can access the data at a time.

  • Mutexes have a reputation for being hard to manage, for 2 reasons

  1. You have to acquire the lock before you can access the data
  2. You have to release the lock when you're done accessing the data

This is hard to maintain because if you forget to release the lock, it can lead to deadlocks.

Rust's strong ownership system and type system, guarantee that you cannot get locking and unlocking wrong.

use std::sync::Mutex;
 
fn main() {
    let m = Mutex::new(5);
 
    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }
 
    println!("m = {:?}", m);
}
  • The Mutex::new(5) function creates a new mutex that contains the value 5.

  • The m.lock().unwrap() function locks the mutex and returns a mutable reference to the value inside the mutex.

  • Note: m.lock().unwrap() is called inside a block, so the lock is released when the block ends.

  • The *num = 6 statement changes the value inside the mutex to 6.

© 2024 Driptanil Datta.All rights reserved

Made with Love ❤️

Last updated on Mon Oct 20 2025