DEV Community

Cover image for πŸ¦€ Rust Master Class - Chapter 14: Concurrency
Oludayo Adeoye
Oludayo Adeoye

Posted on

πŸ¦€ Rust Master Class - Chapter 14: Concurrency

πŸ¦€ Rust Master Class - Chapter 14: Concurrency


Six conversations at a dinner party. I tried to listen to all of them. Couldn't β€” I'm single-threaded. So I learned to switch. That's concurrency: not doing more, but doing better.


Concurrency in Rust is designed to be safe and efficient, leveraging the ownership system to prevent data races at compile time. The sources outline several key mechanisms for handling concurrent execution: threads, message passing (channels), shared state (mutexes), and asynchronous programming (async-await).

1. Threads

Threads allow a program to perform multiple tasks simultaneously. Rust uses the std::thread module for basic thread management.

  • Spawning Threads: You use thread::spawn to create a new thread, passing it a closure containing the code to execute .
  • Thread Control:
    • thread::sleep: Pauses the current thread for a specified duration .
    • thread::yield_now: Voluntarily gives up the thread's current time slice to the OS scheduler, allowing other threads to run .
  • Ownership: When passing data into a thread, you often use the move keyword with the closure to transfer ownership of variables from the parent environment to the spawned thread .

Code Example:

use std::thread;
use std::time::Duration;

fn main() {
    // Create a new variable
    let handle = thread::spawn(|| {
    // Output to console
        println!("Thread started!");
        thread::yield_now(); // Give other threads a chance
    });
    handle.join().unwrap(); // Wait for the thread to finish
}
Enter fullscreen mode Exit fullscreen mode

2. Communication through Channels

Rust implements "message passing" using mpsc (Multiple Producer, Single Consumer) channels. This follows the philosophy: "Do not communicate by sharing memory; instead, share memory by communicating" .

  • Creation: mpsc::channel() returns a tuple containing a Sender (tx) and a Receiver (rx) .
  • Multiple Producers: You can clone the sender (tx.clone()) to allow multiple threads to send messages to the same single receiver .
  • Sending/Receiving: Use tx.send(val) to pass data and rx.recv() to block the current thread until a message is received .

Code Example:

use std::sync::mpsc;
use std::thread;

fn main() {
    // Create a new variable
    let (tx, rx) = mpsc::channel();
    // Create a new variable
    let tx1 = tx.clone();

    thread::spawn(move || {
        tx1.send(10).unwrap();
    });

    // Output to console
    println!("Received: {}", rx.recv().unwrap());
}
Enter fullscreen mode Exit fullscreen mode

3. Sharing State with Mutex and Arc

When threads must access the same data, Rust uses Shared State concurrency.

  • Mutex<T> (Mutual Exclusion): Ensures only one thread can access data at a time by requiring a thread to "lock" the mutex before use .
  • Arc<T> (Atomic Reference Counting): To share a Mutex across multiple threads, it must be wrapped in an Arc. Arc is a thread-safe version of Rc that allows multiple ownership across threads [7-9].
  • Synchronization Barriers: std::sync::Barrier can be used to make multiple threads wait until they all reach a certain point in execution .

Code Example:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Create a new variable
    let counter = Arc::new(Mutex::new(0));
    // Create a mutable variable
    let mut handles = vec![];

    for _ in 0..10 {
    // Create a new variable
        let counter = Arc::clone(&counter);
    // Create a new variable
        let handle = thread::spawn(move || {
    // Create a mutable variable
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }
    // Join threads...
}
Enter fullscreen mode Exit fullscreen mode

4. Async-Await

Async-await is used for non-blocking concurrency, particularly useful for I/O-bound tasks. It allows a single thread to handle many tasks by "yielding" when waiting for a resource .

  • Lazy Execution: Async functions are lazy; calling an async fn does not execute it immediately but returns a Future that must be polled or awaited to run .
  • The Runtime: Rust’s standard library does not include an async runtime. Sources frequently reference Tokio as a common external runtime for executing async tasks .
  • tokio::spawn: Used to run tasks concurrently within the async runtime .
  • tokio::select!: Allows waiting on multiple asynchronous computations at once, returning as soon as the first one completes .

Code Example:

#[tokio::main]
async fn main() {
    // Create a new variable
    let task = tokio::spawn(async {
    // Output to console
        println!("Async task running");
    });

    task.await.unwrap(); // Polling the future to completion
}
Enter fullscreen mode Exit fullscreen mode

πŸ“– Download the full PDF: https://drive.google.com/file/d/1qMZS9TdB7wqPMBJfpUw614X5sKOruLQu/view?usp=sharing

Part 14 of the Rust Master Class series β€” STEM EdTech | Automation Consulting | Rust Tutoring

RustLang #Programming #LearnToCode #STEM #EdTech


πŸ“š Practice Resources

GitHub Repository: https://github.com/PacktPublishing/Rust-Programming-Master-Class-from-Beginner-to-Expert

Try it yourself: https://play.rust-lang.org/

Run the code from this chapter in the Rust playground, then clone the repo to continue your Rust journey!


Part 14 of the Rust Master Class series β€” STEM EdTech | Automation Consulting | Rust Tutoring

RustLang #Programming #LearnToCode #STEM #EdTech

Top comments (0)