Skip to main content
Rust Programming
CHAPTER 22 Beginner

Concurrency in Rust

Updated: May 18, 2026
5 min read

# CHAPTER 22

Concurrency in Rust

1. Chapter Introduction

In older languages like C++, writing concurrent, multi-threaded code is terrifying. One small mistake leads to "Data Races", where two threads modify the same memory simultaneously, causing random crashes that are nearly impossible to debug. Rust is famous for "Fearless Concurrency". By applying the exact same Ownership and Borrowing rules we learned earlier to threads, the compiler guarantees that Data Races are mathematically impossible. If your multi-threaded code compiles, it is safe.

2. Learning Objectives

By the end of this chapter, you will be able to:
  • Spawn new OS threads.
  • Force the main thread to wait using join().
  • Move data into threads using the move keyword.
  • Communicate between threads using Message Passing (Channels).
  • Share memory securely between threads using Mutex and Arc.

3. Spawning Threads

You spawn a new thread using std::thread::spawn. It takes a Closure containing the code you want the new thread to run.
rust
123456789101112131415161718192021
use std::thread;
use std::time::Duration;

fn main() {
    // 1. Spawn a new thread
    let handle = thread::spawn(|| {
        for i in 1..=5 {
            println!("Hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    // 2. Main thread does its own work
    for i in 1..=3 {
        println!("Hi number {} from the MAIN thread!", i);
        thread::sleep(Duration::from_millis(1));
    }

    // 3. Wait for the spawned thread to finish before the program exits
    handle.join().unwrap();
}

*If we did not call handle.join(), the main thread would reach the end of the file and instantly shut down the entire program, killing the spawned thread before it could finish!*

4. The move Keyword

If you want a thread to use data from the main scope, you must transfer Ownership of that data into the thread using the move keyword.
rust
12345678910111213
use std::thread;

fn main() {
    let data = vec![1, 2, 3];

    // 'move' forces the closure to take ownership of 'data'
    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", data);
    });

    // println!("{:?}", data); // ERROR! Main thread no longer owns 'data'
    handle.join().unwrap();
}

5. Message Passing (Channels)

The safest way for threads to communicate is not by sharing memory, but by sending messages to each other. Rust uses mpsc (Multiple Producer, Single Consumer) channels.
rust
12345678910111213141516
use std::sync::mpsc;
use std::thread;

fn main() {
    // tx = Transmitter (Producer), rx = Receiver (Consumer)
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let msg = String::from("Hello from the thread!");
        tx.send(msg).unwrap(); // Sends the message down the channel
    });

    // The main thread waits to receive the message
    let received = rx.recv().unwrap();
    println!("Main thread got: {}", received);
}

6. Shared State (Mutex and Arc)

Sometimes threads *must* share the same piece of memory. To do this safely, we use a Mutex (Mutual Exclusion). A Mutex allows only one thread to access data at any given time. To share the Mutex across multiple threads, we wrap it in an Arc (Atomic Reference Counted smart pointer—the thread-safe version of Rc).
rust
1234567891011121314151617181920212223242526272829
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Create a thread-safe shared counter
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    // Spawn 10 threads
    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        
        let handle = thread::spawn(move || {
            // Lock the mutex. This thread now has exclusive access!
            let mut num = counter_clone.lock().unwrap();
            *num += 1;
            // The lock is automatically released when 'num' goes out of scope here.
        });
        handles.push(handle);
    }

    // Wait for all threads to finish
    for handle in handles {
        handle.join().unwrap();
    }

    // Print final result
    println!("Final counter: {}", *counter.lock().unwrap()); // 10
}

7. Mini Project: Concurrent Task Runner

*(The Mutex example above represents a complete concurrent architecture!)*

8. Common Mistakes

  • Forgetting join(): Spawning threads but forgetting to join them will cause the main thread to exit instantly, making it look like your threads never ran.
  • Using Rc instead of Arc: The compiler will scream at you if you try to pass Rc across threads. Rc is not thread-safe. You must use Arc.

9. Best Practices

  • Prefer Message Passing over Mutexes: Go (Golang) famously says: *"Do not communicate by sharing memory; instead, share memory by communicating."* Using channels (mpsc) is generally easier to reason about and less prone to Deadlocks than using massive Arc<Mutex<T>> structures.

10. Exercises

  1. 1. Create an mpsc channel.
  1. 2. Spawn a thread that sends the number 42 through the transmitter.
  1. 3. In main, receive the number and print it.

11. MCQs with Answers

Question 1

What function is used to spawn a new OS thread in Rust?

Question 2

What method must the main thread call to wait for a spawned thread to finish?

Question 3

What keyword forces a thread's closure to take ownership of environmental variables?

Question 4

In Rust's message passing mpsc::channel(), what does mpsc stand for?

Question 5

When a thread uses tx.send(val), what happens to the ownership of val?

Question 6

If multiple threads need to modify the exact same piece of Heap memory, what must you wrap the data in?

Question 7

What does .lock().unwrap() do on a Mutex?

Question 8

When is a Mutex lock released?

Question 9

To share a Mutex among multiple threads, you wrap it in a smart pointer. Which one is Thread-Safe?

Q10. Why is Rust's concurrency called "Fearless"? a) Because threads don't crash b) Because the exact same Ownership and Borrow Checker rules that apply to single-threaded code apply across threads, making Data Races a compile-time error. Answer: b) Because the compiler prevents Data Races at compile-time.

12. Interview Questions

  • Q: What is a Data Race, and how does Rust's Ownership model completely prevent it?
  • Q: Explain the difference between Rc and Arc. Why can't Rc be used across threads?

13. Summary

Concurrency has historically been the hardest part of systems programming. By enforcing ownership and borrowing rules across thread boundaries, the Rust compiler assumes the burden of thread safety. Whether you choose message passing via channels or shared state via Arc<Mutex<T>>, you can write highly concurrent, multi-core applications with absolute confidence.

14. Next Chapter Recommendation

OS Threads are powerful, but they consume a lot of memory. If you are building a web server that handles 100,000 simultaneous connections, OS threads will crash your machine. In Chapter 23: Async Programming in Rust, we will learn how to handle massive concurrency using async/await and the Tokio runtime.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·