Rust: Concurrent Programming in Rust
Last updated: 2026-08-26
Concurrency is a program's ability to do multiple things "at the same time"—Rust eliminates data races at compile time through its ownership and type systems, allowing you to write concurrent code that is both efficient and safe.
If a single-threaded program is like "one person working in the kitchen from start to finish," then multithreading is like "multiple chefs working at the same time"—some chopping vegetables, some stir-frying, and some plating the dishes. But when there are too many people in the kitchen, things can easily get chaotic: two people might reach for the same knife at the same time, or one person might grab an ingredient that someone else is currently using. Rust's concurrency model is like a "multi-chef kitchen with rules"—each chef has their own tools, ingredients are passed through dedicated channels, and shared seasonings can only be used by one person at a time.
1. What You'll Learn
std::thread::spawnCreate a thread andjoinWait for the thread to finishmoveClosures transfer ownership to the threadmpsc::channelChannel—Many-Producer, Single-Consumer MessagingMutex<T>Mutual Exclusion Locks—Ensuring Thread-Safe Access to Shared DataArc<T>Atomic Reference Counting—AllowingMutex<T>to Be Shared Across Multiple Threads- The Concept of the
Send/SyncTrait—The Foundation of Concurrency Safety in Rust
2. The Story of a Collaborative Document Editing System
(1) Pain Point: Online documentation without concurrency control
Luna's company is developing an online collaborative document editor. Early versions only allowed one person to edit at a time, leading to constant complaints from team members:
- While Alice was writing the proposal, Bob was also editing the same section of text—their edits overwrote each other.
- Carol wants to see real-time updates to the document, but she has to refresh the page every time.
- Dave submitted a change, but Luna had no idea who made it.
- To make matters worse, two editors edited the same piece of content at the same time, and the document crashed.
- Whenever Luna tries to add a "mutually exclusive editing" feature, the code turns into a mess—full of global variables and locks.
"It would be great if we could start a separate thread for each editor, pass modification requests through a channel, and protect the document content with a mutex..."
(2) Approaches to the Rust Concurrency Model
use std::thread;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
fn main() {
// Create a document with shared content(Arc<Mutex<String>>)
let document = Arc::new(Mutex::new(String::from("# Collaborative Documents\n\n")));
// Create a channel,Used to transmit edit requests
let (tx, rx) = mpsc::channel::<String>();
// Receiving Thread:Processing editing requests on an ongoing basis
let doc_for_receiver = Arc::clone(&document);
let receiver = thread::spawn(move || {
for edit in rx {
let mut doc = doc_for_receiver.lock().unwrap();
doc.push_str(&edit);
doc.push('\n');
println!("[Receiver] Edits Applied");
}
});
// Simulating an Editor Thread
let tx1 = tx.clone();
thread::spawn(move || {
tx1.send("- Alice: Added the content for Chapter 1".to_string()).unwrap();
});
let tx2 = tx.clone();
thread::spawn(move || {
tx2.send("- Bob: The title of Chapter 2 has been revised.".to_string()).unwrap();
});
// The main thread also sends a message
tx.send("- Luna: Format Adjustments".to_string()).unwrap();
// Wait until all editors have finished sending their edits
thread::sleep(Duration::from_millis(100));
drop(tx); // Close the sender
receiver.join().unwrap();
// Final Document Content
let final_doc = document.lock().unwrap();
println!("\n=== Final Document ===");
println!("{}", *final_doc);
}
Rust's concurrency solution clearly separates responsibilities: each editor runs in a separate thread, edit requests are passed via
mpsc::channel, and document content is protected byMutex.ArcallowsMutexto be shared across multiple threads. The ownership system ensures that no race conditions occur.
3. Core Concepts
(1) The Rust Concurrency Framework
graph TB
A[Rust Concurrent Programming] --> B[Thread Management]
A --> C[Messaging]
A --> D[Shared Status]
A --> E[Safety Guarantee]
B --> B1["thread::spawn ||"]
B --> B2["join() The Wait Is Over"]
B --> B3["move Closures Transfer Ownership"]
C --> C1["mpsc::channel"]
C --> C2["Sender / Receiver"]
C --> C3["send / recv"]
D --> D1["Mutex<T> Mutex"]
D --> D2["lock() Acquire a lock"]
D --> D3["Arc<T> Atomic Reference Counting"]
E --> E1["Send: Ownership can be transferred across threads"]
E --> E2["Sync: References can be shared across threads"]
E --> E3["Eliminating Data Races at Compile Time"]
(2) Comparison of Three Concurrency Models
| Feature | thread::spawn (thread) | mpsc::channel (channel) | Arc<Mutex<T>> (shared state) |
|---|---|---|---|
| Core Concept | Independent Execution Units | Message Passing | Shared Memory + Locks |
| Data Transfer Methods | move: Transfer ownership via a closure | send/recv: Send a message | lock: Retrieve an internal value |
| Use Cases | Parallel execution of independent tasks | Producer-consumer pattern | Multiple threads accessing the same data |
| Advantages | Makes full use of multi-core CPUs | Decouples senders and receivers | Directly shares any type |
| Disadvantages | Complex inter-thread communication | Not suitable for frequent small data transfers | Potential deadlocks and performance overhead |
| Rust Features | Ownership prevents dangling pointers | The compiler ensures correct usage | Prevents data races at compile time |
(3) Send and Sync trait
| Trait | Meaning | Automatically Implemented Conditions | Non-Automatically Implemented Types |
|---|---|---|---|
| Send | Ownership of a type can be transferred across threads | Implemented automatically for the vast majority of types | Rc<T> (non-atomic reference counting) |
| Sync | References to types can be shared across threads | Implemented automatically for the vast majority of types | RefCell<T> (non-atomic internal mutability) |
| T: Send + Sync | Types that can be safely shared and passed between threads | Arc<T>, Mutex<T> |
Raw pointers *const T, *mut T |
(4) Quick Reference for Selecting Thread Communication Methods
| Communication Method | Type | Data Flow Direction | Lock Required | Applicable Scenarios |
|---|---|---|---|---|
| Channel | mpsc::channel |
Unidirectional (Sender→Receiver) | No | Producer-consumer pattern |
| Channel (multi-producer) | mpsc::channel + tx.clone() |
Multi→Single | No | Multithreaded aggregated results |
| Sharing Status | Arc<Mutex<T>> |
Bidirectional | Yes (mutual exclusion lock) | Multithreaded read/write access to the same data |
| Sharing Status (Read/Write) | Arc<RwLock<T>> |
Bidirectional | Yes (Read/Write Lock) | Read-heavy, write-light scenario |
| Atomic Operations | AtomicUsize et al. |
Bidirectional | No (at the hardware level) | Simple counters/flags |
| Barrier | Barrier |
Synchronization Point | No | Multithreaded Convergence Point |
Rust does not rely on runtime checks to ensure thread safety; instead, it performs compile-time checks using the two marker traits
SendandSync. If a type is notSend, attempting to pass it to another thread will result in a compile-time error.
4. Examples of Concurrent Programming
▶ Example 1: thread::spawn + join—Creating and waiting for a thread (Difficulty ⭐)
Output:
=== Kitchen Renovation Begins ===
[Chef A] Chopping vegetables... Cut #<i>
[Chef B] Stir-frying... Step #<i>
[Head Chef] Plating in progress... Item #<i>
=== Kitchen Shutdown ===
Chef A: <result1>
Chef B: <result2>
All work has been completed!
// ============================================
// Demo:thread::spawn Create a Thread、join The Wait Is Over
// Simulation: Several chefs are preparing different dishes at the same time in the kitchen.
// ============================================
use std::thread;
use std::time::Duration;
fn main() {
println!("=== Kitchen Renovation Begins ===");
// --- 1. Create three threads to perform different tasks ---
let chef1 = thread::spawn(|| {
for i in 1..=3 {
println!("[Chef A] Chopping vegetables... Cut #{}", i);
thread::sleep(Duration::from_millis(50));
}
"A Finished chopping the vegetables"
});
let chef2 = thread::spawn(|| {
for i in 1..=3 {
println!("[Chef B] Stir-frying... Step #{}", i);
thread::sleep(Duration::from_millis(40));
}
"B Finished cooking the stir-fry"
});
// The main thread is also running
for i in 1..=3 {
println!("[Head Chef] Plating in progress... Item #{}", i);
thread::sleep(Duration::from_millis(60));
}
// --- 2. join Wait for all threads to finish and retrieve the return values ---
let result1 = chef1.join().unwrap();
let result2 = chef2.join().unwrap();
println!("\n=== Kitchen Shutdown ===");
println!("Chef A: {}", result1);
println!("Chef B: {}", result2);
println!("All work has been completed!");
}
Output:
=== Kitchen Renovation Begins ===
[Chef A] Chopping vegetables... Cut #1
[Chef B] Stir-frying... Step #1
[Head Chef] Plating in progress... Item #1
[Chef A] Chopping vegetables... Cut #2
[Chef B] Stir-frying... Step #2
[Head Chef] Plating in progress... Item #2
[Chef A] Chopping vegetables... Cut #3
[Chef B] Stir-frying... Step #3
[Head Chef] Plating in progress... Item #3
=== Kitchen Shutdown ===
Chef A: A Finished chopping the vegetables
Chef B: B Finished cooking the stir-fry
All work has been completed!
thread::spawnAccepts a closure and executes it in a new operating system thread.join()Blocks the current thread until the target thread finishes, and returnsResult<T>—if the thread panics,join()returnsErr. The order in which threads are executed is determined by the operating system scheduler and may vary from run to run.
▶ Example 2: mpsc::channel—Messaging (Difficulty ⭐⭐)
Output:
=== Collaborative Document Editor ===
[Server] <user> Inserted: <text>
[Server] <user> Deleted line <line>
[Server] <user> Formatting has been applied: <style>
[Server] Passage Closed,Unsubscribe
All editors have completed their work
=== End of editing session ===
// ============================================
// Demo: mpsc::channel Many-Producer, Single-Consumer Messaging
// Simulation: Multiple editors send modification requests to the document server
// ============================================
use std::thread;
use std::sync::mpsc;
use std::time::Duration;
#[derive(Debug)]
enum EditAction {
Insert { user: String, text: String },
Delete { user: String, line: u32 },
Format { user: String, style: String },
}
fn main() {
println!("=== Collaborative Document Editor ===");
// Create a Channel:Sender Can be cloned,Receiver It is the only one
let (tx, rx) = mpsc::channel::<EditAction>();
// --- 1. Start the receive thread (Document Server) ---
let receiver = thread::spawn(move || {
for action in rx {
match &action {
EditAction::Insert { user, text } => {
println!("[Server] {} Inserted: {}", user, text);
}
EditAction::Delete { user, line } => {
println!("[Server] {} Deleted line {}", user, line);
}
EditAction::Format { user, style } => {
println!("[Server] {} Formatting has been applied: {}", user, style);
}
}
// Simulated Processing Time
thread::sleep(Duration::from_millis(20));
}
println!("[Server] Passage Closed,Unsubscribe");
});
// --- 2. Create the first editor thread (Alice) ---
let tx1 = tx.clone();
let editor1 = thread::spawn(move || {
tx1.send(EditAction::Insert {
user: "Alice".to_string(),
text: "Chapter 1: Rust Introduction".to_string(),
}).unwrap();
thread::sleep(Duration::from_millis(10));
tx1.send(EditAction::Format {
user: "Alice".to_string(),
style: "Bold the title".to_string(),
}).unwrap();
});
// --- 3. Create the second editor thread (Bob) ---
let tx2 = tx.clone();
let editor2 = thread::spawn(move || {
tx2.send(EditAction::Insert {
user: "Bob".to_string(),
text: "Rust is a systems programming language".to_string(),
}).unwrap();
thread::sleep(Duration::from_millis(10));
tx2.send(EditAction::Delete {
user: "Bob".to_string(),
line: 1,
}).unwrap();
});
// --- 4. The main thread also sends a message ---
tx.send(EditAction::Insert {
user: "System".to_string(),
text: "Auto-Save Documents".to_string(),
}).unwrap();
// Wait for the editor thread to finish
editor1.join().unwrap();
editor2.join().unwrap();
// Close the sender - After all Senders are dropped, the Receiver for loop will end automatically
// tx is dropped here (because tx is the last sender on the main thread)
println!("All editors have completed their work");
receiver.join().unwrap();
println!("=== End of editing session ===");
}
// Note: tx is automatically dropped when leaving the scope, channel closes
// If you need to explicitly turn it off,Can be used drop(tx)
Output:
=== Collaborative Document Editor ===
[Server] System Inserted: Auto-Save Documents
[Server] Alice Inserted: Chapter 1: Rust Introduction
[Server] Bob Inserted: Rust is a systems programming language
[Server] Alice Formatting has been applied: Bold the title
[Server] Bob Deleted line 1
All editors have completed their work
[Server] Passage Closed,Unsubscribe
=== End of editing session ===
mpscstands for "Multiple Producer, Single Consumer."Sendercan be copied to multiple threads viaclone, but there can only be oneReceiver. When allSenderare dropped, the channel automatically closes, and the iterator forReceiverterminates.sendreturnsResult—if the receiving end has closed, it returnsErr.
▶ Example 3: Arc<Mutex<T>>—Thread-Safe Access to Shared State (Difficulty ⭐⭐⭐)
Output:
=== Shared Counter Demo ===
[Workers<id>] Current Count: <*num>
[Workers<id>] Work Completed
=== Final Results ===
Final counter value: <*final_count>
Expected value: 50 (5 threads x 10 times)
// ============================================
// Demo: Arc<Mutex<T>> Safely Sharing Data Among Multiple Threads
// Simulation: Multiple workers modifying a shared counter simultaneously
// ============================================
use std::thread;
use std::sync::{Arc, Mutex};
use std::time::Duration;
fn main() {
println!("=== Shared Counter Demo ===");
// Use Arc<Mutex<i32>> to wrap shared data
let counter = Arc::new(Mutex::new(0i32));
let mut handles = vec![];
// --- Start 5 threads,Each thread increments the counter by 10 ---
for id in 0..5 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
for _ in 0..10 {
// lock() Acquire a mutex lock——If the lock is held by another thread,The current thread will block while waiting
let mut num = counter_clone.lock().unwrap();
*num += 1;
println!("[Workers{}] Current Count: {}", id, *num);
// Locks are automatically released when they go out of scope
}
println!("[Workers{}] Work Completed", id);
});
handles.push(handle);
}
// --- Wait for all threads to finish ---
for handle in handles {
handle.join().unwrap();
}
// --- Read the final results ---
let final_count = counter.lock().unwrap();
println!("\n=== Final Results ===");
println!("Final counter value: {}", *final_count);
println!("Expected value: {} (5 threads x 10 times)", 5 * 10);
}
Output:
=== Shared Counter Demo ===
[Workers0] Current Count: 1
[Workers0] Current Count: 2
[Workers1] Current Count: 3
[Workers1] Current Count: 4
[Workers0] Current Count: 5
... (The intermediate output varies depending on thread scheduling)
[Workers4] Current Count: 50
[Workers4] Work Completed
=== Final Results ===
Final counter value: 50
Expected value: 50 (5 threads x 10 times)
Mutex<T>provides mutual exclusion—only one thread can access the internal data at a time.lock()returnsMutexGuard<T>, which implementsDerefandDrop: the lock is automatically released when the scope is exited.Arc<T>(Atomic Reference Counted) is the multithreaded version ofRc<T>, using atomic operations to ensure thread safety for reference counting.Arc::cloneincrements the reference count without copying data.
▶ Example 4: Inter-thread Collaboration—The Producer-Consumer Pattern (Difficulty ⭐⭐⭐)
Output:
Producer1 Done
Producer2 Done
=== Consumer Receipt ===
Received: <msg>
All producers have finished, consumer section ended
// ============================================
// Producer-Consumer: Multiple producers + Single consumer
// ============================================
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let tx2 = tx.clone();
thread::spawn(move || {
let items = vec!["Apple", "Banana", "Orange"];
for item in items {
tx.send(format!("Producer1: {}", item)).unwrap();
thread::sleep(Duration::from_millis(100));
}
println!("Producer1 Done");
});
thread::spawn(move || {
let items = vec!["Watermelon", "Grapes"];
for item in items {
tx2.send(format!("Producer2: {}", item)).unwrap();
thread::sleep(Duration::from_millis(150));
}
println!("Producer2 Done");
});
println!("=== Consumer Receipt ===");
for msg in rx {
println!(" Received: {}", msg);
}
println!("All producers have finished, consumer section ended");
}
Output:
=== Consumer Receipt ===
Received: Producer1: Apple
Received: Producer2: Watermelon
Received: Producer1: Banana
Received: Producer1: Orange
Received: Producer2: Grapes
Producer1 Done
Producer2 Done
All producers have finished, consumer section ended
tx.clone()Creates multiple senders to implement a multi-producer pattern. When all senders (txandtx2) are dropped, therxiterator automatically terminates—there is no need to manually send an "end" signal.
▶ Example 5: Comprehensive Exercise—Parallel Data Processing (Difficulty ⭐⭐⭐)
Output:
The various thread sections and: <partials>
=== Serial Summation ===
1 to 1000 sum: <sequential_sum>
=== Parallel Summation (4 Thread) ===
Parallel Summation Results: <parallel_result>
=== 10M Data Performance Comparison ===
Serial: <seq_time>
Parallel (8Thread): <par_time>
// ============================================
// Parallel Computing: Multithreaded Sharded Summation
// ============================================
use std::sync::{Arc, Mutex};
use std::thread;
fn parallel_sum(data: &[i64], num_threads: usize) -> i64 {
let chunk_size = (data.len() + num_threads - 1) / num_threads;
let result = Arc::new(Mutex::new(0i64));
let mut handles = Vec::new();
for i in 0..num_threads {
let chunk = data[i * chunk_size..(i * chunk_size + chunk_size).min(data.len())].to_vec();
let result = Arc::clone(&result);
handles.push(thread::spawn(move || {
let partial: i64 = chunk.iter().sum();
*result.lock().unwrap() += partial;
partial
}));
}
let mut partials = Vec::new();
for handle in handles {
partials.push(handle.join().unwrap());
}
println!("The various thread sections and: {:?}", partials);
*result.lock().unwrap()
}
fn main() {
let data: Vec<i64> = (1..=1000).collect();
let sequential_sum: i64 = data.iter().sum();
println!("=== Serial Summation ===");
println!("1 to 1000 sum: {}", sequential_sum);
println!("\n=== Parallel Summation (4 Thread) ===");
let parallel_result = parallel_sum(&data, 4);
println!("Parallel Summation Results: {}", parallel_result);
assert_eq!(sequential_sum, parallel_result);
let data2: Vec<i64> = (1..=10_000_000).collect();
let start = std::time::Instant::now();
let _ = data2.iter().sum::<i64>();
let seq_time = start.elapsed();
let start = std::time::Instant::now();
let _ = parallel_sum(&data2, 8);
let par_time = start.elapsed();
println!("\n=== 10M Data Performance Comparison ===");
println!("Serial: {:?}", seq_time);
println!("Parallel (8Thread): {:?}", par_time);
}
Output:
=== Serial Summation ===
1 to 1000 sum: 500500
=== Parallel Summation (4 Thread) ===
The various thread sections and: [78126, 218874, 109374, 94126]
Parallel Summation Results: 500500
=== 10M Data Performance Comparison ===
Serial: [Time]
Parallel (8Thread): [Time]
parallel_sumPartition the data; each thread calculates a partial sum, which is then accumulated into the total result viaArc<Mutex<i64>>.chunk_sizeRound up to ensure that all data is accounted for.assert_eq!Verify that the parallel result matches the serial result. For large datasets, multithreading can significantly improve performance.
❓ FAQ
thread::spawn What is the relationship between the threads created and the main thread?mpsc::channel's send and recv block?send typically does not block (it returns immediately when there is space in the buffer), while recv blocks until a message is received.Mutex<T> and Arc<Mutex<T>> have to be used together?Mutex cause a deadlock?Send and Sync?Send means "ownership can be transferred across threads," and Sync means "references can be shared across threads."📖 Summary
thread::spawnCreates an operating system thread and accepts a closure as the execution body;join()waits for the thread to finish and retrieves the resultmoveClosures Transfer ownership of values to new threads to avoid dangling references—this is a direct application of Rust's ownership system in concurrency.mpsc::channelprovides a multi-producer, single-consumer channel;sendsends messages, andrecvreceives messages; the channel automatically closes when all senders are droppedMutex<T>provides mutual exclusion;lock()acquires the lock (blocks);MutexGuardautomatically releases the lock when it goes out of scopeArc<T>is the thread-safe version ofRc<T>; it uses atomic operations to manage reference counts and serves as a bridge for sharingMutex<T>across multiple threads.Send/Syncare Rust's concurrency-safe marker traits—Sendallows ownership transfer across threads,Syncallows reference sharing across threads, and the compiler performs checks at compile time.
📝 Exercises
- Difficulty ⭐: Write a program that creates three threads to calculate the sums of 1..=10, 11..=20, and 21..=30, respectively. The main thread uses
jointo wait for all threads to finish, then aggregates the three partial sums and prints the final result. - Difficulty ⭐⭐: Use
mpsc::channelto implement a "task dispatcher." Create 1 producer thread (which generates 10 tasks numbered 1–10) and 2 consumer threads (each consumer receives a task number from the channel and prints "[Consumer X] processing task #N"). Ensure that all tasks are processed. - Difficulty ⭐⭐⭐: Implement a "shared bank account" system. Use
Arc<Mutex<f64>>as the shared balance. Create 4 threads to simulate deposit operations (each thread deposits a random amount between 10 and 100 yuan). The main thread waits for all deposit threads to finish before reading the final balance. Additional requirement: Print the balance change before and after each deposit to verify that there is no race condition (final balance = initial balance + sum of all deposits).