Thread
use std::thread;
fn main() {
thread::spawn(||{
for i in 1..=9{ print!("a{i} "); }
});
}
std::thread::spawn(|| ...);와 같은 형태로 사용합니다.- 클로저 함수에 쓰레드로 동작시킬 루틴을 넣으면 됩니다.
sleep
thread::sleep(Duration::from_millis(100));
thread::sleep(dur)를 이용해서 잠시 멈추게 할 수 있습니다.Duration::from_millis(1)같은 duration을 이용해 시간을 정할 수 있습니다.
join
use std::thread;
fn main() {
let handle = thread::spawn(||{
for i in 1..=9{ print!("a{i} "); }
});
let _ = handle.join();
}
- 쓰레드가 끝날 때까지 기다립니다.
use std::thread;
fn main() {
let handle = thread::spawn(|| {
let mut sum = 0;
for i in 1..=100 { sum += i; }
sum //이 값이 join으로 전달
});
let result = handle.join().unwrap();
println!("sum = {}", result); //sum = 5050
}
- 쓰레드의 리턴 값을 가져올 수도 있습니다.
쓰레드간 데이터 '전송'
- 메인에서 생성된 변수를 쓰레드의 클로저에서 그냥 사용
-
let handle = thread::spawn(move || { // move 사용 println!("v: {:?}", v); // v는 main에서 정의된 변수로, move에 의해 소유권이 이동되었음 }); - spawn에서 클로저를 생성할 때
move키워드를 써야 합니다. (소유권 강제 이동) - 메인에서의 데이터를 쓰레드에서 '참조' 형태로 borrow해서 사용하는 것을 허락하지 않습니다.
-
- mpsc (multi-producer, single-consumer) 채널 사용
-
let (tx, rx) = mpsc::channel(); let tx1 = tx.clone(); let handle1 = thread::spawn (move||{ ... tx1.send(val).unwrap(); // 송신. val 소유권 이동 }); let handle2 = thread::spawn (move||{ ... tx.send(val).unwrap(); // 송신. val 소유권 이동 }); let sum = rx.recv().unwrap(); // 수신.mpsc::channel()에 의해 리턴된Sender의send(data)메서드를 이용합니다.- 채널은 일종의 FIFO(First-in First-out) 버퍼
-
for r in rx { println!("{}", r); }- 이런 식으로 코드를 작성해서 버퍼 메시지를 모두 읽을 수 있습니다. 신기하게도 버퍼가 비어있다고 끝나지 않습니다.
- 루프가 끝나는 조건은 "모든 Sender가 사라졌을 때"입니다.
rx.recv(): 가장 오래된 메시지 하나 읽음
-
- mpmc (multi-producer, multi-consumer) 채널 사용
- 아직 안정화가 되어있지 않습니다. (2026.9월 기준)
- nightly 전용 unstable 기능이라고 합니다.
쓰레드간 데이터 '공유'
| 단일 쓰레드 | 멀티 쓰레드 | |
|---|---|---|
| 공유만 | Rc<T> | Arc<T> |
| 공유 + 수정 | Rc<RefCell<T>> | Arc<Mutex<T>> |
Rc (Reference counted)
- 레퍼런싱하는 메모리에의 접근을 카운터를 써서 관리합니다.
- 여러 개의 변수들이 동일한 힙 메모리 영역을 참조하며 사용할 수 있습니다.
use std::rc::Rc;
fn main() {
let s1 = Rc::new(String::from("hello"));
println!("{}", Rc::strong_count(&s1)); //1
let s2 = Rc::clone(&s1);
println!("{}", Rc::strong_count(&s1)); //2
{
let s3 = Rc::clone(&s1);
println!("{}", Rc::strong_count(&s1)); //3
} //여기서 s3가 사라지면서 카운트가 줄어든다
println!("{}", Rc::strong_count(&s1)); //2
println!("{} {}", s1, s2);
}
Rc<T>로 감싼 값은 읽을 수만 있고 고칠 수 없습니다.Rc::clone(): Rc 객체가 추가로 복제되는 것이 아니고, 단지 Rc 내의 'Ref count'의 값만 증가합니다.
Arc (Atomic Reference Counted)
- Rc와 마찬가지로 힙 메모리 공간을 여러 변수가 참조할 수 있습니다.
- Rc와의 차이는 참조 카운트를 원자적으로 세느냐 아니냐 입니다.
-
let mut handles = vec![]; for i in 0..=2 { let ss = Arc::clone(&s); handles.push(thread::spawn(move || { println!("{}: {:?}", i, ss); })); } for h in handles { h.join().unwrap(); }
Mutex (Mutual Exclusion)
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut n = m.lock().unwrap();
*n = 6;
}
println!("m = {:?}", m); //m = Mutex { data: 6, poisoned: false, .. }
}
std::sync::Mutex를 통해 뮤텍스를 사용할 수 있습니다.- Rust의 Mutex에는
unlock()메서드가 없습니다. 좀 특이합니다.- 락이 풀리는 시점은
MutexGuard가Drop될 때 (즉 스코프를 벗어날 때) - 락을 일찍 풀고 싶을 때는 스코프를 좁히거나
drop(n);을 명시적으로 호출하면 됩니다.
- 락이 풀리는 시점은
m.lock()뒤에.unwrap()가 필요한가?- 락을 잡고 있던 쓰레드가 패닉에 빠지는 경우 락을 잡는 걸 실패할 수 있다.
- 출력에
poisoned: false란 값이 있는데 이는 오염된 데이터인지 알려줍니다.
Arc + Mutex 예제
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
fn main(){
let m = Arc::new(Mutex::new(0));
let mut handles = vec![];
for i in 0..10 {
let mm = Arc::clone(&m);
let handle = thread::spawn(move || {
let mut n = mm.lock().unwrap(); // <- lock
thread::sleep(Duration::from_millis(1));
println!("Thread {}: Before increasing. num = {}", i, n);
*n += 1;
println!("Thread {}: After increasing. num = {}", i, n);
});
handles.push(handle);
}
println!("{:?}", Arc::strong_count(&m));
handles.into_iter().for_each(|h| h.join().unwrap());
println!("Result: {}", *m.lock().unwrap());
}
비동기 프로그래밍
Rust에서는 비동기 프로그래밍을 지원하는 Future 트레잇과 async/await 기능이 있습니다. Green 쓰레드의 실행과 스케줄링을 하는 기능은 없어서 외부 라이브러리를 사용해야 합니다.
tokio
$cargo add tokio --features full
- 프로젝트로 이동 후 tokio를 add 합니다.
[dependencies] tokio = { version = "1", features = ["full"] }
Cargo.toml파일에 다음과 같이 추가됩니다.
async fn say_hello(){
println!("hello");
}
#[tokio::main]
async fn main() {
say_hello().await;
}
- main 함수에 대해 async로 지정하기 위해서는
#[tokio::main]매크로를 지정해줘야 합니다. .await를 사용해야 실행이 됩니다.- 자바스크립트에서는 await가 없어도 async 함수 호출이 바로 되지만 Rust에서는 그렇지 않습니다.
- (다른 언어와 사용법이 조금 달라 헷갈릴 수 있습니다.)
use tokio::time::{sleep, Duration};
async fn work(name: &str, ms: u64) {
sleep(Duration::from_millis(ms)).await;
println!("{} 완료", name);
}
#[tokio::main]
async fn main() {
//(1) 순차 실행: 총 300ms가 걸린다
work("A", 100).await;
work("B", 200).await;
//(2) 동시 실행: 총 200ms면 끝난다
let h1 = tokio::spawn(work("C", 100));
let h2 = tokio::spawn(work("D", 200));
h1.await.unwrap();
h2.await.unwrap();
}
tokio::spawn으로 각각을 별도의 작업(task)으로 띄울 수 있습니다.
#[tokio::main]
async fn main() {
// 여기까지는 Core 쓰레드에서 수행되는 영역이다.
let blocking_task = tokio::task::spawn_blocking(|| {
// 여기에 블로킹되어 수행되는 코드를 넣으면 된다.
});
// Blocking 쓰레드가 생성되고, blocking_task가 수행 대기 상태로 된다.
blocking_task.await.unwrap();
}
- Core 쓰레드는 여러 작업이 나눠 쓰는 쓰레드입니다. 그래서 어떤 작업이 여기서 오래 붙잡고 있으면 다른 작업들이 전부 멈춥니다.
- CPU를 오래 쓰는 계산
std::thread::sleep처럼 쓰레드를 재우는 함수- tokio 버전이 없는 라이브러리의 블로킹 함수
- 이런 것들을
spawn_blocking으로 감싸면 별도의 OS 쓰레드로 넘어가서 Core 쓰레드를 막지 않습니다.tokio::fs,tokio::net같은 비동기 버전이 있는 작업은spawn_blocking을 쓸 필요 없이 그냥.await하면 됩니다!
주의할 점
std::thread::sleep을 쓰면 안됩니다.
std::thread::sleep: OS 쓰레드 자체를 재웁니다. 그 쓰레드에서 돌던 다른 비동기 작업들까지 전부 함께 멈춥니다.tokio::time::sleep: 그 작업만 잠재우고 런타임은 같은 쓰레드에서 다른 작업을 계속 처리합니다.
비동기 파일 읽기/쓰기
| 하는 일 | 동기 (std) | 비동기 (tokio) |
|---|---|---|
| 전체 읽기 | fs::read_to_string | tokio::fs::read_to_string |
| 전체 쓰기 | fs::write | tokio::fs::write |
| 파일 열기 | File::open | tokio::fs::File::open |
| 조금씩 읽기 | Read 트레잇 | AsyncReadExt 트레잇 |
| 버퍼 사용 | BufReader | tokio::io::BufReader |
읽기 예제 1
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>>{
let txt = tokio::fs::read_to_string("hello.txt").await?;
println!("File contents: \n{}", txt);
Ok(())
}
읽기 예제 2
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let contents = tokio::fs::read("hello.txt").await?;
let txt = String::from_utf8(contents)?;
println!("File contents: \n{}", txt);
Ok(())
}
read는 파일 전체 내용을 읽어서Result<Vec<u8>>을 리턴합니다.
쓰기 예제
use tokio::fs;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>>{
fs::write("tmp.txt", b"Hello world!").await?;
Ok(())
}
- 파일이 이미 있으면 기존 내용을 모두 지우고 새로 씁니다.
- 뒤에 이어서 쓸려면
OpenOptions을 써야합니다.
- 뒤에 이어서 쓸려면
- 한글을 쓰려면
b"..."대신"헬로 러스트".as_bytes()를 써야 합니다.- 바이트 문자열 리터럴(
b"...")은 아스키만 담을 수 있기 때문입니다.
- 바이트 문자열 리터럴(