Rust Concurrency | Threads, Channels, Arc, and Mutex
이 글의 핵심
Rust concurrency tutorial: std::thread, mpsc channels, Arc and Mutex, parallel sums, pitfalls, Send/Sync, and when to use rayon or Tokio in production.
Introduction
Rust supports concurrent programming while preserving memory safety in safe code.
1. Threads
Basic threading
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(100));
}
});
for i in 1..5 {
println!("main: {}", i);
thread::sleep(Duration::from_millis(100));
}
handle.join().unwrap();
}
Moving data into a thread
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("vector: {:?}", v);
});
handle.join().unwrap();
// `v` was moved into the closure and cannot be used here
}
2. Channels
Basic channel
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hello");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("received: {}", received);
}
Multiple messages
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_millis(100));
}
});
for received in rx {
println!("received: {}", received);
}
}
Multiple senders
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
let tx2 = tx.clone();
thread::spawn(move || {
tx.send(String::from("thread 1")).unwrap();
});
thread::spawn(move || {
tx2.send(String::from("thread 2")).unwrap();
});
for received in rx {
println!("received: {}", received);
}
}
3. Arc<T> and Mutex<T>
Mutex (mutual exclusion)
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
} // lock released here
println!("m = {:?}", m);
}
Arc + Mutex
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("result: {}", *counter.lock().unwrap()); // 10
}
4. Hands-on example
Parallel sum
use std::thread;
use std::sync::{Arc, Mutex};
fn parallel_sum(numbers: Vec<i32>) -> i32 {
let chunk_size = numbers.len() / 4;
let numbers = Arc::new(numbers);
let result = Arc::new(Mutex::new(0));
let mut handles = vec![];
for i in 0..4 {
let numbers = Arc::clone(&numbers);
let result = Arc::clone(&result);
let handle = thread::spawn(move || {
let start = i * chunk_size;
let end = if i == 3 { numbers.len() } else { (i + 1) * chunk_size };
let sum: i32 = numbers[start..end].iter().sum();
let mut total = result.lock().unwrap();
*total += sum;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
*result.lock().unwrap()
}
fn main() {
let numbers: Vec<i32> = (1..=1000).collect();
let sum = parallel_sum(numbers);
println!("sum: {}", sum); // 500500
}
Production notes
Chunked parallel sum (stdlib only)
std::mpsc::Receiver is not cloneable, so a “many workers, one queue” pattern is awkward with raw channels alone. Pre-chunk the data and give each thread its own slice instead:
use std::thread;
fn parallel_sum(nums: Vec<i32>, workers: usize) -> i32 {
assert!(workers > 0);
let chunk_size = (nums.len() + workers - 1) / workers;
let chunks: Vec<Vec<i32>> = nums
.chunks(chunk_size.max(1))
.map(|c| c.to_vec())
.collect();
let handles: Vec<_> = chunks
.into_iter()
.map(|chunk| {
thread::spawn(move || chunk.iter().copied().sum::<i32>())
})
.collect();
handles.into_iter().map(|h| h.join().unwrap()).sum()
}
fn main() {
let nums: Vec<i32> = (1..=10_000).collect();
let total = parallel_sum(nums, 4);
println!("sum: {}", total);
}
Common mistakes
- Holding a
Mutexlock across I/O, starving other workers. - Sharing data across threads without
Arc(or another safe mechanism). - Deadlocks when locking multiple mutexes in inconsistent order.
Caveats
- A poisoned
lock()often means another thread panicked while holding the lock. Send/Syncbounds often show up first in closure captures—learn what they mean.
In production
- CPU-bound work: often rayon; I/O-heavy work: tokio (or another async runtime).
- Prefer message passing and minimal shared mutable state.
Comparison
| Tool | Use case |
|---|---|
| OS threads | CPU-bound work, isolation |
| Tokio tasks | Lots of async I/O waiting |
| Processes | Strong isolation, crash containment |
Further reading
Summary
Takeaways
- thread::spawn: create OS threads
- mpsc::channel: communicate between threads
- Arc: atomically reference-counted sharing
- Mutex: mutual exclusion for mutable shared state
- join: wait for threads to finish
Next steps
Related posts
- C++ memory model
- C++ multithreading basics
- C++ vs Rust: full comparison
- Java multithreading | Thread, Runnable, Executor
- C++ atomics
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Rust concurrency tutorial: std::thread, mpsc channels, Arc and Mutex, parallel sums, pitfalls, Send/Sync, and when to us…
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- Rust 컬렉션 | Vec, HashMap, HashSet
- Rust 비동기 프로그래밍 | async/await, Tokio
- Rust 동시성 | Thread, Channel, Arc, Mutex
Keywords Covered in This Article (Related Search Terms)
This article covers Rust, Concurrency, Threads, Mutex, Channel, Arc.