← Rust 강의 목록으로
🦀
모던 Rust
모던 · 선수: 21강

22. async/await + tokio 입문

async/await 는 IO 바운드 작업을 효율적으로 동시에 처리하는 비동기 프로그래밍 모델입니다. Rust 의 async 는 런타임이 표준에 포함돼 있지 않아 tokio 같은 외부 크레잇을 선택해 씁니다. 이 강의에서 future 의 개념, async fn 작성, .await 사용, tokio runtime 진입점까지 한 번에 정리합니다.

RustasyncawaittokioFuture비동기
소요 시간
약 2시간
난이도
📊 중급
선수 조건
🎯 21강
결과물
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:

toml
[dependencies]
tokio = { version = "1", features = ["full"] }
rust
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! 으로 동시 진행:

rust
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 와 ? 연산자:

rust
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 바운드에 큰 효과

과제

  1. 100ms / 50ms / 200ms 씩 자는 세 작업을 join! 으로 묶어 가장 긴 200ms 안에 끝남을 확인
  2. tokio::fs::read_to_string 으로 두 파일을 동시에 읽어 길이를 합산
  3. ?로 에러를 전파하는 async fn 으로 reqwest::get 한 번 호출하기 (Cargo.toml 에 reqwest 추가)
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗