Rust에서 소유권과 수명을 다루다 보면 임시값(temporary value)이 언제 소멸되는지 정확히 이해하는 것이 중요합니다. 특히 let 문과 while let(또는 if let, match) 사이에는 미묘하지만 중요한 차이가 있습니다.
문제의 발단: 멀티스레드 워커 구현
The Rust Programming Language에서 다음 두 코드의 차이를 설명합니다:
// 코드 A: let 문 사용
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let handle = thread::spawn(move || loop {
let task = receiver.lock().unwrap().recv().unwrap();
println!("Worker {} executing job.", id);
task();
});
Worker { id, thread: handle }
}
}
// 코드 B: while let 사용
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let handle = thread::spawn(move || {
while let Ok(task) = receiver.lock().unwrap().recv() {
println!("Worker {} executing job.", id);
task();
}
});
Worker { id, thread: handle }
}
}
코드 B는 컴파일되고 실행되지만, 느린 요청이 있을 때 다른 요청들이 대기하는 문제가 발생합니다. 이유는 MutexGuard의 수명 때문입니다.
핵심 원리: 임시값 소멸 시점
Mutex::lock()은 LockResult<MutexGuard<T>>를 반환하며, MutexGuard가 살아있는 동안 뮤텍스가 잠겨 있습니다. 문제는 임시값이 언제 drop되는가에 있습니다:
let문: 오른쪽 표현식의 임시값은let문이 끝나면 즉시dropwhile let/if let/match: 관련된 코드 블록이 끝날 때까지 임시값 유지
직접 검증해보기
다음 실험으로 이 차이를 명확히 확인할 수 있습니다:
struct Resource(String);
struct Handle(String);
impl Resource {
fn acquire(&self) -> Result<Handle, ()> {
Ok(Handle(self.0.clone()))
}
}
impl Drop for Resource {
fn drop(&mut self) {
println!(" [drop Resource(\"{}\")]", self.0);
}
}
impl Drop for Handle {
fn drop(&mut self) {
println!(" [drop Handle(\"{}\")]", self.0);
}
}
fn main() {
println!("=== while let 테스트 ===");
let data: Result<_, ()> = Ok(Resource("A".into()));
while let _ = data.unwrap().acquire().unwrap() {
println!(" while 본문 실행 중");
break; // 한 번만 실행
}
println!("while 블록 종료 후");
println!("\n=== let 문 테스트 ===");
let data2: Result<_, ()> = Ok(Resource("B".into()));
let _ = data2.unwrap().acquire().unwrap();
println!("let 문 다음 줄");
println!("\n=== match 테스트 ===");
let data3: Result<_, ()> = Ok(Resource("C".into()));
match data3.unwrap().acquire().unwrap() {
_ => println!(" match arm 실행 중"),
}
println!("match 블록 종료 후");
}
실행 결과 분석
=== while let 테스트 ===
[drop Handle("A")]
[drop Resource("A")]
while 본문 실행 중
while 블록 종료 후
=== let 문 테스트 ===
[drop Handle("B")]
[drop Resource("B")]
let 문 다음 줄
=== match 테스트 ===
[drop Handle("C")]
[drop Resource("C")]
match 블록 종료 후
결과 해석
| 구문 | 임시값 소멸 시점 | 특징 |
|---|---|---|
let _ = expr; | let 문 종료 즉시 | 바로 drop |
while let _ = expr { ... } | 각 반복의 블록 종료 시 | 조건 평가 후 블록 끝까지 유지 |
if let _ = expr { ... } | if 블록 종료 시 | 블록 내에서 임시값 참조 가능 |
match expr { ... } | match 블록 종료 시 | arm 전체에서 임시값 유지 |
실전 적용: 올바른 워커 구현
따라서 뮤텍스를 짧게 잡고 싶다면 while let 대신 loop + let 조합을 사용해야 합니다:
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let handle = thread::spawn(move || loop {
// lock()의 임시값(MutexGuard)이 let 문 끝에 즉시 해제됨
let task = match receiver.lock().unwrap().recv() {
Ok(job) => job,
Err(_) => break,
};
println!("Worker {} executing job.", id);
task(); // job 실행 중에도 뮤텍스는 이미 해제됨
});
Worker { id, thread: handle }
}
}
또는 while let을 사용하되 중간에 let으로 명시적으로 분리:
while let Ok(guard) = receiver.lock() {
let task = guard.recv().unwrap(); // guard를 명시적 바인딩
drop(guard); // 명시적 해제 (선택적)
task();
}
언더스코어 바인딩의 함정
참고로 let _ = expr;와 let _x = expr;, let x = expr;도 미묘한 차이가 있습니다:
let _ = some_value; // 즉시 drop될 수 있음 (경고 없음)
let _unused = some_value; // 명명된 바인딩, 컴파일러가 미사용 경고
let used = some_value; // 명시적 바인딩, 스코프 끝까지 유지
하지만 임시값 소멸 시점의 핵심 차이는 let 문법 자체와 while let 패턴 매칭 구문의 구조적 차이에 있습니다.