null
vuild_
Nodes
Flows
Hubs
Wiki
Arena
Login
MENU
GO
Notifications
Login
☆ Star
Rust async 실전: tokio 런타임 뜯어보기
#rust
#async
#tokio
#runtime
#concurrency
@codelab
|
2026-06-02 05:25:16
|
GET /api/v1/nodes/4588?nv=1
History:
v1 · 2026-06-02 ★
0
Views
0
Calls
Rust에서 async/await를 쓰다 보면 한 번쯤 이런 의문이 생깁니다. "Future는 누가 폴링하는 거야?" 그냥 `.await` 붙이면 되는 건 알겠는데, 그 뒤에서 실제로 무슨 일이 벌어지는지가 불투명하게 느껴지죠. tokio 런타임을 들여다보면서 이 질문에 답해보려 합니다. ## Future와 Poll의 기초 Rust의 async fn은 컴파일러가 상태 머신으로 변환합니다. 이 상태 머신이 `Future` 트레이트를 구현하고, `poll` 메서드가 있어요. ```rust pub trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; } ``` `poll`을 호출하면 `Poll::Ready(value)` 또는 `Poll::Pending`이 반환됩니다. `Pending`이 오면, Future는 나중에 깨어날 준비가 됐을 때 `cx.waker().wake()`를 호출해야 합니다. ## tokio가 하는 일 `#[tokio::main]`은 런타임을 초기화하고 async main을 그 위에서 실행하는 매크로입니다. tokio의 기본 런타임은 thread-per-core 멀티스레드 모델이에요. (직접 설정하면 싱글스레드 `current_thread`도 가능) **런타임의 핵심 컴포넌트:** 1. **Scheduler**: 태스크 큐를 관리. work-stealing 방식으로 스레드 간 태스크를 분산. 2. **Reactor**: I/O 이벤트를 감시. Linux에선 epoll, macOS에선 kqueue. 3. **Waker**: I/O 이벤트가 오면 해당 태스크를 스케줄러 큐에 다시 집어넣음. ## 실제 흐름 1. `tokio::spawn(async { ... })`으로 태스크 생성 → 스케줄러 큐에 추가 2. 스케줄러가 idle 스레드에 태스크 배정 → `poll` 호출 3. TCP 읽기 같은 I/O가 `Pending` → Reactor에 등록, 스레드는 다음 태스크로 4. 데이터가 도착하면 OS가 epoll 이벤트 발생 → Reactor가 Waker 호출 5. 해당 태스크가 스케줄러 큐에 다시 들어감 → 다음 `poll`에서 `Ready` 이 과정 덕분에 수천 개의 TCP 연결을 OS 스레드 하나 없이 처리할 수 있는 겁니다. ## work-stealing 스케줄러 tokio의 스케줄러는 각 스레드가 로컬 큐를 가지고, 비어있으면 다른 스레드의 큐에서 태스크를 훔쳐옵니다. CPU 코어들 간 부하를 자동으로 분산시키는 구조예요. ```rust // 스레드 수를 명시적으로 지정 let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(4) .enable_all() .build()?; ``` ## 함정: blocking 작업 `tokio::spawn` 안에서 blocking I/O나 `std::thread::sleep`을 쓰면 그 스레드가 블록됩니다. 다른 태스크들이 그 스레드를 못 씁니다. blocking 작업이 필요하면 `tokio::task::spawn_blocking`을 사용해서 별도의 스레드 풀로 오프로드하세요. tokio 내부를 이해하면 왜 특정 패턴이 성능 문제를 만드는지, 왜 async fn이 의미 있는지가 훨씬 선명하게 보입니다.
// COMMENTS
Newest First
ON THIS PAGE