0Pricing
Learn Rust Coding · Lesson

Fearless Concurrency with Threads

Dive into Rust's primitives for concurrent programming, including threads and message passing, ensuring data race freedom.

Fearless Concurrency with Threads is a free Learn Rust Coding lesson on CoddyKit — lesson 3 of 3. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Learn Rust Coding learning path, one of 3 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Intro to Concurrency

Welcome to Fearless Concurrency with Threads! In this lesson, we'll learn how to make your Rust programs do multiple things at once.

Concurrency means executing multiple computations seemingly at the same time. It's vital for responsive applications and utilizing modern multi-core processors.

  • Threads are lightweight units of execution within a program.
  • Each thread can run a separate part of your code.
  • Rust's ownership system helps prevent common concurrency bugs.

Creating Your First Thread

Rust provides std::thread::spawn to create new threads. You pass it a closure (an anonymous function) that contains the code the new thread should run.

Try running this example to see threads in action:

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

fn main() {
  println!("Hello from the main thread!");

  thread::spawn(|| {
    for i in 1..=5 {
      println!("Hi number {} from the spawned thread!", i);
      thread::sleep(Duration::from_millis(1));
    }
  });

  for i in 1..=3 {
    println!("Hi number {} from the main thread!", i);
    thread::sleep(Duration::from_millis(1));
  }
}

Waiting for Threads with `join`

In the last example, the spawned thread might not finish before the main thread exits. This is because the main thread doesn't wait for spawned threads by default.

To ensure a spawned thread completes its work, we use the join() method on its JoinHandle. This blocks the current thread until the joined thread finishes.

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

fn main() {
  let handle = thread::spawn(|| {
    for i in 1..=5 {
      println!("Thread: {}", i);
      thread::sleep(Duration::from_millis(1));
    }
  });

  for i in 1..=3 {
    println!("Main: {}", i);
    thread::sleep(Duration::from_millis(1));
  }

  handle.join().unwrap(); // Wait for the spawned thread to finish
  println!("Spawned thread has finished!");
}

Moving Data into Threads

When you use variables from the environment inside a spawn closure, Rust's ownership rules come into play. By default, closures try to borrow variables.

If the spawned thread outlives the main thread's scope where the variable was defined, this can lead to a dangling reference. To fix this, use the move keyword before the closure's parameters.

  • move forces the closure to take ownership of its captured variables.
  • This ensures the data is valid for the entire lifetime of the new thread.
use std::thread;

fn main() {
  let data = String::from("Hello from outer scope");

  let handle = thread::spawn(move || { // Use 'move' to take ownership of 'data'
    println!("Data in thread: {}", data);
  });

  handle.join().unwrap();
  // println!("Data after thread: {}", data); // This would cause a compile error!
  println!("Main thread finished.");
}

Communicating with Message Passing

While sharing data directly between threads (shared state) is possible in Rust, it requires careful synchronization (e.g., with Mutex and Arc, covered in other lessons).

A safer and often simpler approach for concurrency is message passing. Threads communicate by sending messages to each other, avoiding direct shared memory access.

  • One thread sends data.
  • Another thread receives data.
  • Rust's standard library provides std::sync::mpsc for this.

Setting Up an `mpsc` Channel

std::sync::mpsc stands for Multiple Producer, Single Consumer. This means many threads can send messages, but only one thread can receive them.

To create a channel, you call mpsc::channel(). It returns a tuple containing:

  • A Sender (tx): Used to send messages.
  • A Receiver (rx): Used to receive messages.

Let's see how to create one and send a simple message.

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

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

  thread::spawn(move || {
    let val = String::from("hi");
    tx.send(val).unwrap(); // Send the message
    // println!("val is {}", val); // Error: val moved to tx.send()
  });

  // Receive the message in the main thread
  let received = rx.recv().unwrap();
  println!("Got: {}", received);
}

Sending Multiple Messages

You can send multiple messages through the same channel. The Receiver has methods to handle this:

  • rx.recv(): Blocks the current thread until a message is received. Returns a Result.
  • rx.try_recv(): Non-blocking. Returns a Result immediately, either with a message or an error if no message is available.
  • You can also iterate directly over the Receiver to get messages until the sender closes.
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
  let (tx, rx) = mpsc::channel();

  thread::spawn(move || {
    let msgs = vec!["hi", "from", "the", "thread"];
    for msg in msgs {
      tx.send(String::from(msg)).unwrap();
      thread::sleep(Duration::from_millis(10));
    }
  });

  // Iterate over the receiver to get all messages
  for received in rx {
    println!("Got: {}", received);
  }
  println!("All messages received!");
}

Multiple Producers, Single Consumer

The 'MP' in mpsc means Multiple Producers. You can clone a Sender to have multiple threads send messages to the same Receiver.

Each cloned Sender shares access to the same channel, allowing concurrent message sending from different threads.

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
  let (tx, rx) = mpsc::channel();

  let tx1 = tx.clone(); // Clone the sender for another thread
  thread::spawn(move || {
    tx1.send(String::from("hello from tx1")).unwrap();
  });

  thread::spawn(move || {
    tx.send(String::from("hello from tx2")).unwrap();
  });

  // Collect all messages
  let mut received_messages: Vec<String> = vec![];
  for received in rx {
    received_messages.push(received);
    if received_messages.len() == 2 { break; } // Assuming 2 messages for this example
  }
  println!("Received: {:?}", received_messages);
}

Fearless Concurrency with Rust

Rust's ownership and type system are crucial for achieving fearless concurrency. By enforcing rules at compile time, Rust prevents common concurrency bugs like data races.

  • Ownership prevents multiple mutable references to the same data.
  • move keyword ensures data is safely transferred to a new thread.
  • Message passing (mpsc) avoids shared memory entirely, making communication safe by design.

These mechanisms allow you to write concurrent code with confidence, knowing the compiler will catch many potential issues.

Concurrency Concepts Check

Which of the following statements about Rust's concurrency primitives are TRUE?

Recap: Threads & Message Passing

Great job! You've learned the fundamentals of fearless concurrency in Rust:

  • We create new threads using std::thread::spawn.
  • We use JoinHandle::join() to wait for a thread to complete.
  • The move keyword safely transfers ownership of data into a thread.
  • std::sync::mpsc provides a robust message passing system for inter-thread communication.
  • Rust's ownership rules prevent data races at compile time, making concurrent programming safer.

This lesson provides a solid foundation for building responsive and efficient applications. Keep practicing!

Frequently asked questions

Is the “Fearless Concurrency with Threads” lesson free?

Yes — the full text of “Fearless Concurrency with Threads” is free to read here on the web, and the Learn Rust Coding course includes 3 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Learn Rust Coding course, upgrade to CoddyKit PRO.

What will I learn in “Fearless Concurrency with Threads”?

Dive into Rust's primitives for concurrent programming, including threads and message passing, ensuring data race freedom. You practise Learn Rust Coding with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Learn Rust Coding?

No prior experience is required. Learn Rust Coding on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 3, so you can start here or from the beginning and move at your own pace.

How long does the “Fearless Concurrency with Threads” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Learn Rust Coding lesson?

Yes. Every Learn Rust Coding lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Box, Rc, and Arc Smart Pointers
  2. Interior Mutability: RefCell, Cell
  3. Fearless Concurrency with Threads
← Back to Learn Rust Coding