Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

16. 비동기/Await 핵심 🔴

학습 목표:

  • Rust의 Future와 Go의 고루틴, Python의 asyncio가 어떻게 다른지 이해합니다.
  • Tokio 런타임 기초: 태스크 스폰(Spawn), join!, 런타임 설정을 배웁니다.
  • 비동기 프로그래밍의 흔한 실수를 방지하는 법을 익힙니다.
  • CPU 집약적인 작업을 spawn_blocking으로 분리하는 시점을 결정합니다.

퓨처(Future)와 런타임의 관계

Rust의 비동기 모델은 다른 언어와 근본적으로 다릅니다.

  1. Future는 게으른(Lazy) 상태 머신: async fn을 호출해도 아무 일도 일어나지 않습니다. 실행을 위해서는 이를 폴(Poll)해 줄 누군가가 필요합니다.
  2. 런타임이 필요함: 표준 라이브러리는 Future 트레이트만 정의할 뿐, 실행기는 제공하지 않습니다. tokio가 가장 널리 쓰이는 실행기입니다.
  3. async fn은 문법적 설탕(Sugar): 컴파일러가 이를 Future 트레이트를 구현하는 복잡한 상태 머신 구조체로 변환합니다.

비동기 프로그램의 흔한 실수와 해결법

실수원인해결책
비동기 안에서 블로킹thread::sleep이나 무거운 연산이 실행기 스레드를 점유함spawn_blocking이나 rayon 사용
Send 경계 에러.await 지점을 건너갈 때 RcMutexGuard 같은 !Send 타입을 들고 있음.await 전에 해당 변수를 드롭하도록 스코프 조정
퓨처를 실행하지 않음.await나 스폰(Spawn) 없이 함수만 호출함반드시 .await 하거나 tokio::spawn으로 실행
의도치 않은 순차 실행let a = foo().await; let b = bar().await;처럼 작성tokio::join!을 사용해 동시에 실행
#![allow(unused)]
fn main() {
// ❌ 비동기 실행기를 멈추게 하는 나쁜 코드
async fn bad() {
    std::thread::sleep(Duration::from_secs(5)); // 스레드 전체가 멈춤!
}

// ✅ 블로킹 작업을 별도 풀로 보내는 좋은 코드
async fn good() {
    tokio::task::spawn_blocking(|| {
        std::thread::sleep(Duration::from_secs(5)); // 전용 스레드에서 실행
    }).await.unwrap();
}
}

태스크 스폰과 구조적 동시성

tokio::spawn은 OS 스레드보다 훨씬 가벼운 비동기 태스크를 생성합니다. 여러 태스크를 효율적으로 관리하기 위한 매크로들을 활용하세요.

  • join!: 모든 퓨처가 완료될 때까지 기다립니다.
  • try_join!: 하나라도 에러가 발생하면 즉시 중단하고 에러를 반환합니다.
  • select!: 가장 먼저 완료되는 퓨처의 결과를 반환합니다(타임아웃이나 취소 로직에 유용).

📝 연습 문제: 타임아웃이 있는 동시 페처 ★ (~25분)

세 개의 비동기 태스크를 tokio::spawn으로 실행하고, 이를 tokio::try_join!으로 묶어 보세요. 전체 작업에 tokio::time::timeout을 걸어 5초 안에 완료되지 않으면 에러를 반환해야 합니다.


📌 요약

  • .await가 없으면 아무 일도 일어나지 않습니다. 퓨처는 능동적으로 실행되지 않는 게으른 객체입니다.
  • 비동기 코드에서 std::thread::sleep을 절대 사용하지 마세요. 반드시 tokio::time::sleep을 써야 합니다.
  • 무거운 CPU 연산은 **spawn_blocking**으로 격리하여 실행기 스레드가 멈추지 않게 하세요.
  • Send 제약 조건은 퓨처가 스레드 간에 이동할 수 있음을 보장하는 Rust 특유의 안전 장치입니다.