Rust Concurrency | Threads, Channels, Arc, and Mutex
이 글의 핵심
Learn Rust concurrency: spawning threads, message passing with channels, shared state with Arc and Mutex, parallel aggregation patterns, and production tips.
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 threadsmpsc::channel: communicate between threadsArc: atomically reference-counted sharingMutex: mutual exclusion for mutable shared statejoin: wait for threads to finish
Next steps
- Async Rust
- Rust web development
- Rust testing
Related posts
- C++ memory model
- C++ multithreading basics
- C++ vs Rust: full comparison
- Java multithreading | Thread, Runnable, Executor
- C++ atomics