Await 표현식

Await 표현식 (.await)

비동기 코드를 작성하다 보면 "이 작업이 끝날 때까지 잠시 멈춰 있다가 결과를 받아오고 싶다"는 순간이 오는데, 그때 쓰는 게 await 표현식이에요. 어떤 값이 준비될 때까지 계산을 일시 중단(suspend) 했다가, 준비되면 다시 진행하는 구문이에요.

출처: Rust Reference - Await expressions

본문

await 표현식은 std::future::IntoFuture를 구현한 대상이 값이 생길 준비가 될 때까지 계산을 중단시키는 구문적 장치예요.

구문 (Syntax)

AwaitExpression → Expression . await

구조를 보면, 먼저 IntoFuture 트레이트를 구현하는 타입을 가진 표현식(future operand) 이 오고, 그다음 . 토큰, 마지막에 await 키워드가 와요.

let value = some_future.await;

await 표현식은 비동기 컨텍스트(async context) 안에서만 쓸 수 있어요. 예를 들어 async fn, async 클로저, async 블록 같은 곳이에요. 그 밖의 일반 함수에서는 쓸 수 없어요.

수행되는 일

await 표현식이 평가되면 실제로 이런 일이 순서대로 벌어져요.

  1. future 피연산자에 IntoFuture::into_future를 호출해서 future를 만든다.
  2. future를 tmp라는 임시 값으로 평가한다.
  3. Pin::new_uncheckedtmp핀(pin) 한다.
  4. 핀된 future를 현재 task 컨텍스트를 넘기며 Future::poll 메서드로 폴링한다.
  5. 만약 pollPoll::Pending을 돌려주면, future도 Poll::Pending을 돌려주며 상태를 중단시킨다. 그래서 주변 async 컨텍스트가 다시 폴링될 때 3번 단계부터 실행을 재개한다.
  6. 그 외의 경우 pollPoll::Ready를 돌려줬어야 하며, 그 안에 담긴 값이 await 표현식 자체의 결과가 된다.

핵심은, await는 단순히 "기다렸다가 쓰는" 게 아니라 미래의 어느 시점에 폴링을 재개하는 루틴이라는 거예요. 이벤트 루프가 다시 돌아왔을 때 중단됐던 지점부터 이어서 진행하는 구조예요.

Task 컨텍스트

task 컨텍스트는 "현재 async 컨텍스트가 폴링될 때 공급된 Context"를 말해요. await 표현식은 async 컨텍스트에서만 허용되므로, 그만큼 항상 사용 가능한 task 컨텍스트가 존재해요.

근사적인 desugaring

실은 await 표현식은 훨씬 단순한 형태로 "펼쳐져(desugar) 있다"고 봐도 돼요. 다음은 그 근사적인 desugaring이에요 (표준 비규범 예시).

match operand.into_future() {
    mut pinned => loop {
        let mut pin = unsafe { Pin::new_unchecked(&mut pinned) };
        match Pin::future::poll(Pin::borrow(&mut pin), &mut current_context) {
            Poll::Ready(r) => break r,
            Poll::Pending => yield Poll::Pending,
        }
    }
}

여기서 yield 의사 코드는 Poll::Pending을 돌려주고, 다시 호출되면 그 지점부터 실행을 재개해요. current_context 변수는 async 환경에서 가져온 컨텍스트를 가리켜요.

에디션 차이

await 표현식은 Rust 2018 에디션부터 사용할 수 있어요. 2015 에디션에서는 쓸 수 없고요.

더 알아보기 (Learn more)