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
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Rust concurrency tutorial: std::thread, mpsc channels, Arc and Mutex, parallel sums, pitfalls, Send/Sync, and when to us… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Rust 컬렉션 | Vec, HashMap, HashSet
- Rust 비동기 프로그래밍 | async/await, Tokio
- Rust 동시성 | Thread, Channel, Arc, Mutex
이 글에서 다루는 키워드 (관련 검색어)
Rust, Concurrency, Threads, Mutex, Channel, Arc 등으로 검색하시면 이 글이 도움이 됩니다.