Concurrency in Rust: Threads, Goroutines, async/await — Full Breakdown

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Concurrency in Rust: From Mutexes to async/await



When it comes to high-performance systems, Rust instantly takes center stage. But what makes it truly unique? It's concurrency in Rust — an approach where you get the speed of C and memory safety without sacrificing code readability. In this article, we'll break down three main tools: classic threads, lightweight goroutines (in the context of Rust — asynchronous tasks), and modern async/await. Let's go!



Why Isn't Rust Afraid of Concurrency?

Unlike many languages, Rust solves the problem of data races at the compiler level. The ownership and borrowing system guarantees that you cannot accidentally modify data from two threads simultaneously. This isn't magic — it's strict static checking. That's why multithreading in Rust is considered one of the safest.



1. Threads (std::thread)

Classic OS threads are the foundation. In Rust, they are implemented through the std::thread module. Each thread is a separate lightweight process that can run in parallel on different CPU cores.



Example: Spawning Threads

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

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

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

handle.join().unwrap();}


Here, thread::spawn creates a new thread, and join waits for it to finish. Simple, but there's a nuance: threads are "heavy." Each thread consumes at least 2-8 MB of stack memory. For thousands of concurrent tasks, this isn't suitable.



When to Use Threads?

  • When you need true parallelism (CPU-bound tasks).
  • When the number of threads does not exceed the number of cores * 2.
  • When working with blocking operations (file reading, synchronous calls).


2. Goroutines in Rust: Myth or Reality?

Strictly speaking, Rust doesn't have goroutines in the same sense as Go. But it has asynchronous tasks that behave almost the same way: they are lightweight, switch cooperatively (without OS involvement), and can be launched by the thousands on a single thread. For this, the Tokio or async-std runtime is used.



Example: Asynchronous Task (Goroutine) via Tokio

use tokio::task;

#[tokio::main]async fn main() { let handle = task::spawn(async { for i in 1..10 { println!("Goroutine: {}", i); tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; } });

for i in 1..5 { println!("Main: {}", i); tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; }

handle.await.unwrap();}


Notice: task::spawn creates a lightweight task that runs within Tokio's thread pool. Memory overhead is about 2-3 KB per task. This allows running 100,000 "goroutines" without issues. Unlike Go, there is no built-in scheduler here — you choose the runtime yourself.



How Are Rust Goroutines Different from Go?

  • In Rust — tasks are cooperative: they yield control themselves via .await.
  • In Go — goroutines are preemptive: the scheduler interrupts them at I/O points.
  • Rust gives more control: you can choose a single-threaded or multi-threaded runtime.


3. Async/await: The Heart of Async Rust

The keywords async and .await turn synchronous code into asynchronous code without

Blogs

Book Recommendations