Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

런타임을 차단하지 마세요 (Don’t Block the Runtime)

다시 양보 지점(Yield points) 이야기로 돌아가 봅시다. 스레드와 달리, Rust의 태스크는 선점(Preemption)될 수 없습니다.

tokio 런타임이 임의로 태스크를 중단시키고 다른 태스크를 실행하도록 결정할 수 없다는 뜻입니다. 제어권은 태스크가 명시적으로 양보할 때만 실행기에게 돌아갑니다. 즉, Future::pollPoll::Pending을 반환하거나, 비동기 함수 내에서 .await를 호출할 때만 제어권이 넘어갑니다.

이런 구조는 런타임에 위험 요인이 될 수 있습니다. 만약 어떤 태스크가 절대 양보하지 않는다면, 런타임은 영영 다른 태스크를 실행할 기회를 얻지 못할 것입니다. 이를 **런타임 차단(Blocking the runtime)**이라고 부릅니다.

’차단(Blocking)’이란 무엇일까요?

얼마나 오래 걸려야 ’너무 길다’고 할 수 있을까요? 태스크가 양보 없이 어느 정도의 시간을 보내면 문제가 될까요?

이는 런타임 설정, 애플리케이션의 성격, 진행 중인 태스크의 수 등 많은 요인에 따라 달라집니다. 하지만 일반적인 가이드라인으로는, 양보 지점 사이의 실행 시간이 **100 마이크로초(μs)**를 넘지 않도록 노력하는 것이 좋습니다.

차단 시 발생하는 문제

런타임이 차단되면 다음과 같은 심각한 문제가 발생할 수 있습니다:

  • 교착 상태 (Deadlock): 양보하지 않는 태스크 A가 태스크 B의 완료를 기다리고 있고, 태스크 B는 태스크 A가 제어권을 양보하기를 기다리고 있다면 전체 시스템이 멈춰버립니다. 다른 스레드에서 태스크를 실행할 수 없는 상황이라면 영구적인 교착 상태에 빠집니다.
  • 기아 현상 (Starvation): 다른 태스크들이 실행 기회를 잡지 못해 응답이 지연됩니다. 이는 전체적인 성능 저하와 높은 응답 지연 시간(Tail latency)으로 이어집니다.

차단은 생각보다 교묘합니다

어떤 작업들은 비동기 코드에서 특히 주의해야 합니다. 대표적으로 다음과 같은 것들이 있습니다:

  • 동기 I/O: 작업 완료까지 얼마나 걸릴지 예측할 수 없으며, 100 마이크로초를 훌쩍 넘길 가능성이 매우 높습니다.
  • 고비용 CPU 연산: 계산량이 많은 작업들입니다.

CPU 연산의 경우 판단이 쉽지 않을 때가 있습니다. 예를 들어 몇 개의 요소를 가진 벡터를 정렬하는 것은 문제가 없지만, 수십억 개의 데이터를 정렬한다면 이야기가 완전히 달라지죠.

차단을 피하는 방법

그렇다면 차단이 발생할 위험이 있는 작업을 수행해야 할 때는 어떻게 해야 할까요? 그 작업을 다른 스레드로 옮겨야 합니다. 이때 tokio가 비동기 태스크를 실행하는 데 사용하는 소위 ’런타임 스레드’는 건드리지 않는 것이 좋습니다.

대신 tokio는 이런 목적을 위해 **차단 풀(Blocking pool)**이라는 전용 스레드 풀을 제공합니다. tokio::task::spawn_blocking 함수를 사용하여 차단 풀에서 동기 작업을 실행할 수 있습니다. spawn_blocking은 작업이 완료되었을 때 그 결과를 담은 퓨처를 반환합니다.

use tokio::task;

fn expensive_computation() -> u64 {
    // [...]
}

async fn run() {
    // 무거운 작업을 차단 풀로 보냅니다.
    let handle = task::spawn_blocking(expensive_computation);
    // 그동안 비동기 런타임에서는 다른 작업을 수행할 수 있습니다.
    let result = handle.await.unwrap();
}

차단 풀은 스레드를 재사용하도록 설계되었습니다. 따라서 매번 std::thread::spawn으로 새 스레드를 만드는 것보다 효율적입니다. 스레드 생성 비용을 여러 번의 호출에 걸쳐 분담하기 때문입니다.

더 읽어보기

Exercise

The exercise for this section is located in 08_futures/05_blocking