22. async/await + tokio 입문
async/await 는 IO 바운드 작업을 효율적으로 동시에 처리하는 비동기 프로그래밍 모델입니다. Rust 의 async 는 런타임이 표준에 포함돼 있지 않아 tokio 같은 외부 크레잇을 선택해 씁니다. 이 강의에서 future 의 개념, async fn 작성, .await 사용, tokio runtime 진입점까지 한 번에 정리합니다.
이 강의에서 배우는 것
- 1async fn 으로 비동기 함수를 정의한다
- 2.await 로 Future 의 완료를 기다린다
- 3tokio::main 어트리뷰트로 main 을 비동기 진입점으로 만든다
- 4join! / try_join! 로 여러 작업을 동시에 진행한다
- 5동시성(concurrency) 과 병렬성(parallelism) 의 차이를 안다
소개
스레드는 OS 가 관리하는 무거운 단위(한 개 = 수 MB 스택)라 수천 개를 띄우면 시스템이 못 견딥니다. 비동기는 한 스레드 위에 수천·수만 개의 "작업" 을 띄우고, IO 대기 중인 작업이 자동으로 CPU 를 양보합니다 — 웹 서버·DB 클라이언트·네트워크 도구가 async 의 주요 활용처.
핵심 개념
1) Future — 아직 완료되지 않은 값
`async fn` 의 반환 타입은 항상 `impl Future<Output = T>` 입니다. Future 는 폴링되어 "아직 안 끝남(Pending)" 또는 "끝남(Ready(T))" 을 알려줍니다.
2) .await — 완료까지 기다림
Future 뒤에 `.await` 를 붙이면 결과가 준비될 때까지 **이 작업은 일시정지**, 런타임이 다른 작업으로 CPU 를 돌립니다. 동기 코드와 거의 같은 모양으로 비동기를 작성할 수 있게 만드는 핵심 문법.
3) 런타임 선택 — tokio
Rust 의 async 는 언어 차원의 문법이고, 실제 실행 엔진(런타임) 은 표준 라이브러리에 없습니다. **tokio** 가 사실상 표준 — 멀티스레드 워커 풀 + IO 폴링.
4) 동시성 vs 병렬성
| 용어 | 의미 |
|---|---|
| 동시성(concurrency) | 여러 작업을 번갈아 진행 (한 스레드여도 가능) |
| 병렬성(parallelism) | 물리적으로 동시 실행 (여러 코어) |
async 는 동시성 중심. tokio 멀티스레드 런타임은 동시성 + 병렬성 둘 다.
핵심 예제
Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }use tokio::time::{sleep, Duration};
async fn say_after(ms: u64, msg: &str) {
sleep(Duration::from_millis(ms)).await;
println!("{}", msg);
}
#[tokio::main]
async fn main() {
say_after(100, "hello").await;
say_after(50, "world").await;
// 출력: hello → world (각자 await 으로 순차)
}join! 으로 동시 진행:
use tokio::time::{sleep, Duration};
async fn task(name: &str, ms: u64) {
sleep(Duration::from_millis(ms)).await;
println!("{} done after {}ms", name, ms);
}
#[tokio::main]
async fn main() {
tokio::join!(
task("A", 100),
task("B", 50),
task("C", 80),
);
// 출력 순서: B (50ms) → C (80ms) → A (100ms) — 100ms 안에 끝
}Result 반환 async 와 ? 연산자:
use std::error::Error;
use tokio::fs;
async fn read_file(path: &str) -> Result<String, Box<dyn Error>> {
let s = fs::read_to_string(path).await?;
Ok(s)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let s = read_file("data.txt").await?;
println!("{} bytes", s.len());
Ok(())
}자주 하는 실수
Q. async fn 을 그냥 호출하면 실행 안 돼요
A. async fn 호출은 **Future 를 만들 뿐** 실행은 안 합니다. `.await` 를 붙이거나 tokio::spawn 으로 런타임에 등록해야 실제 진행.
Q. tokio 와 async-std 중 뭘 쓰나요?
A. 현재 사실상의 표준은 **tokio**. 가장 큰 생태계(axum/reqwest/sqlx 등) 가 tokio 위에 만들어져 있습니다.
Q. async 함수 안에서 std::thread::sleep 을 써도 되나요?
A. **안 됩니다.** 그 스레드 전체가 막혀 다른 async 작업까지 멈춥니다. 비동기 sleep 인 `tokio::time::sleep(...).await` 를 사용하세요.
Q. Send 가 아닌 타입을 async 안에서 들고 있어도 되나요?
A. tokio 멀티스레드 런타임에서는 await 지점을 가로질러 살아있는 값이 Send 여야 합니다. 안 되면 컴파일 에러로 알려줍니다.
정리
- async fn 의 반환 = impl Future<Output = T>
- .await 로 완료 기다림 (그 사이 다른 작업 진행)
- tokio 가 사실상 표준 런타임 — #[tokio::main] 으로 진입
- join! / try_join! 로 여러 작업을 동시에 진행
- 동시성 ≠ 병렬성, async 는 IO 바운드에 큰 효과
과제
- 100ms / 50ms / 200ms 씩 자는 세 작업을 join! 으로 묶어 가장 긴 200ms 안에 끝남을 확인
- tokio::fs::read_to_string 으로 두 파일을 동시에 읽어 길이를 합산
- ?로 에러를 전파하는 async fn 으로 reqwest::get 한 번 호출하기 (Cargo.toml 에 reqwest 추가)