async/.await 기초

async/.await는 동기적 코드처럼 생긴 비동기 함수들을 작성하는 데 쓰이는 러스트 내장 도구입니다. async는 코드 블록을 Future라는 트레잇을 구현하는 상태기계로 변환해줍니다. 동기적 메소드 안에서 블록하는 함수를 호출한다면 전체 스레드가 블록되지만, Future는 블록될 때 스레드를 놓아버리므로 다른 Future가 실행될 수 있습니다.

Cargo.toml 파일에 의존성을 추가해 봅시다.

[dependencies]
futures = "0.3"

비동기 함수를 만들기 위해, async fn 문법을 사용합니다.


#![allow(unused)]
fn main() {
async fn do_something() { /* ... */ }
}

async fn이 반환하는 값은 한 개의 Future 객체입니다. 코드가 실제로 동작하게 하려면, executor로 Future 객체를 실행해야 합니다.

// `block_on`는 제공받은 future를 실행하여 완성될 때까지 현재의 스레드를
// 블록한다. 다른 종류의 executor는 여러 개의 future를 같은 스레드 안에서
// 스케줄링을 한다던가 하는 식으로 보다 복잡하게 동작한다.
use futures::executor::block_on;

async fn hello_world() {
    println!("hello, world!");
}

fn main() {
    let future = hello_world(); // 아무것도 출력되지 않음
    block_on(future); // `future` 가 실행되어 "hello, world!"가 출력됨
}

async fn 안에서 Future 트레잇을 구현한 다른 타입이 완성(예시: 다른 async fn의 출력 등)될 때까지 기다리려면 .await을 사용하면 됩니다. block on과 달리, .await는 현재의 스레드를 블록하지 않고, 대신에 해당 future가 완성될 때까지 비동기적으로 기다립니다. 이렇게 하면 해당 future가 현재 진행될 수 없는 상황에서도 다른 태스크들이 실행될 수 있습니다.

예를 들어, 세 개의 async fn(learn_song, sing_song 그리고 dance)이 있다고 칩시다.

async fn learn_song() -> Song { /* ... */ }
async fn sing_song(song: Song) { /* ... */ }
async fn dance() { /* ... */ }

노래를 배우고 부르며, 춤을 추기위한 방법 중에 하나는 각각을 수행할 때마다 블록하는 것입니다.

fn main() {
    let song = block_on(learn_song());
    block_on(sing_song(song));
    block_on(dance());
}

그러나, 이 방법으로는 최선의 성능을 낼 수 없습니다. 오직 한 번에 한 가지만 한다구요! 우리가 노래를 부르기 전에 먼저 노래를 배워야 하는 것은 맞지만, 춤은 노래를 배우거나 부르면서도 출 수 있습니다. 이를 위해, 우리는 동시에 수행될 수 있는 두 개의 다른 async fn을 만들면 됩니다.

async fn learn_and_sing() {
    // 노래를 부르기 전에 노래를 배울 때까지 기다림.
    // 스레드를 블록하지 않기 위해 `block_on` 대신에 `.await`을 사용한다. 이렇게
    // 하면, `춤`을 동시에 출 수 있다.
    let song = learn_song().await;
    sing_song(song).await;
}

async fn async_main() {
    let f1 = learn_and_sing();
    let f2 = dance();

    // `join!`은 `.await`와 비슷하지만 여러 개의 future를 동시에 기다릴 수 있다.
    // `learn_and_sing` future에서 일시적으로 블록되었더라도, `dance` future는
    // 현재의 스레드를 가져올 것이다. `dance`가 블록되면, `learn_and_sing`은 다시
    // 스레드를 가져올 수 있다. 둘 다 블록되면, `async_main`이 블록되고,
    // executor에게 스레드를 양보할 것이다.
    futures::join!(f1, f2);
}

fn main() {
    block_on(async_main());
}

이 예제에서, 노래 배우기는 노래 부르기보다 먼저 동작해야 하지만, 노래 배우기와 부르기는 춤추기와 같은 시간에 동작할 수 있습니다. 만약 learn_and_sing안에서 learn_song().await말고, block_on(learn_song())을 사용했다면, 해당 스레드는 learn_song이 동작하는 동안에는 아무것도 할 수 없었을 것이고, 그렇다면 춤추기를 노래와 동시에 수행할 수 없었을 것입니다. 하지만 우리는 learn_song future를 .await함으로써, learn_song이 블록되었을지라도 다른 태스크들이 현재의 스래드에서 실행되게 할 수 있습니다. 이 방법으로, 여러 개의 future를 한 개의 스레드에서 동시에 실행하여 완성할 수 있습니다.