Tokio는 Rust의 비동기 프로그램을 실행하는 런타임이다. async 함수가 만드는 Future를 진행시키는 스케줄러와 네트워크·타이머·동기화 도구를 제공한다. async/await는 언어의 표현 방식이고 Tokio는 그 작업을 실제로 진행시키는 실행 환경이라는 구분이 중요하다.
동시성과 병렬성
네트워크 응답처럼 기다리는 작업은 대기 중 실행권을 다른 작업에 넘길 수 있다. 많은 연결마다 전용 스레드를 하나씩 만드는 부담을 줄이는 데 유리하다. 그러나 한 작업이 오래 계산하거나 동기 API에서 멈추면 스케줄러가 다른 작업을 충분히 처리하지 못할 수 있다. 멀티스레드 런타임을 쓰는 것만으로 이런 문제가 사라지는 것은 아니다.
장점은 타이머·채널·소켓을 같은 비동기 모델로 조합하고 여러 라이브러리와 연결하기 쉽다는 것이다. 한계는 취소와 공유 상태를 명시적으로 설계해야 하고, 비동기 제약으로 타입과 수명 관리가 복잡해질 수 있다는 점이다. CPU 중심의 긴 계산은 별도 작업 풀이나 spawn_blocking 같은 경계를 검토하되 작업 수 역시 제한해야 한다.
두 대출 창구의 응답을 함께 기다리기
Tokio 1에서 macros, rt, time 기능을 켠 Cargo 프로젝트를 가정한다. 실제 네트워크 대신 타이머로 두 가상 창구의 대기를 표현한 예제이며, 지연 시간은 성능 측정값이 아니다. main.rs에 저장하고 cargo run으로 실행할 수 있다.
[dependencies]
tokio = { version = "1", features = ["macros", "rt", "time"] }use std::time::Duration;
async fn desk(name: &'static str, delay_ms: u64) -> &'static str {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
name
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let (east, west) = tokio::join!(desk("동쪽 창구", 120), desk("서쪽 창구", 180));
println!("조회 완료: {east}, {west}");
}join!은 같은 작업 안에서 두 Future를 함께 진행하고 둘 다 완료된 뒤 결과를 반환한다. 여기서는 한 스레드에서도 대기가 겹칠 수 있다는 점이 핵심이다. 서로 다른 CPU 코어에서 두 함수를 병렬 실행한다는 보장은 아니며 반환 순서는 인자 위치에 대응한다.
자주 생기는 오류
std::thread::sleep을 비동기 함수에서 사용하면 해당 스레드가 그대로 멈춘다. tokio::time::sleep처럼 실행권을 넘기는 API와 구분한다. 공유 잠금을 잡은 채 긴 await를 수행하면 다른 작업이 불필요하게 기다릴 수 있다. spawn한 작업의 JoinHandle을 다루지 않으면 완료·실패를 놓치기 쉬우며 프로그램이 종료되면 작업이 끝까지 실행된다고 가정해서는 안 된다.
시간 제한이 발생해 Future가 버려져도 외부 시스템에서 이미 처리한 요청까지 자동으로 되돌아가는 것은 아니다. 예약이나 결제 같은 변경은 재시도 키와 서버의 중복 처리 방지 규칙을 따로 가져야 한다. 비동기는 대기를 효율적으로 다루는 도구이며 업무의 원자성을 대신하지 않는다.