Rust 임시값 소멸 시점: let과 while let의 미묘한 차이

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 문이 끝나면 즉시 drop
  • while 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 패턴 매칭 구문의 구조적 차이에 있습니다.

태그: Rust Mutex Ownership Lifetime Drop

6월 24일 04:59에 게시됨