16. 비동기/Await 핵심 🔴
학습 목표:
- Rust의
Future와 Go의 고루틴, Python의 asyncio가 어떻게 다른지 이해합니다.- Tokio 런타임 기초: 태스크 스폰(Spawn),
join!, 런타임 설정을 배웁니다.- 비동기 프로그래밍의 흔한 실수를 방지하는 법을 익힙니다.
- CPU 집약적인 작업을
spawn_blocking으로 분리하는 시점을 결정합니다.
퓨처(Future)와 런타임의 관계
Rust의 비동기 모델은 다른 언어와 근본적으로 다릅니다.
Future는 게으른(Lazy) 상태 머신:async fn을 호출해도 아무 일도 일어나지 않습니다. 실행을 위해서는 이를 폴(Poll)해 줄 누군가가 필요합니다.- 런타임이 필요함: 표준 라이브러리는
Future트레이트만 정의할 뿐, 실행기는 제공하지 않습니다.tokio가 가장 널리 쓰이는 실행기입니다. async fn은 문법적 설탕(Sugar): 컴파일러가 이를Future트레이트를 구현하는 복잡한 상태 머신 구조체로 변환합니다.
비동기 프로그램의 흔한 실수와 해결법
| 실수 | 원인 | 해결책 |
|---|---|---|
| 비동기 안에서 블로킹 | thread::sleep이나 무거운 연산이 실행기 스레드를 점유함 | spawn_blocking이나 rayon 사용 |
Send 경계 에러 | .await 지점을 건너갈 때 Rc나 MutexGuard 같은 !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 특유의 안전 장치입니다.