async/.await

1 장에서 우리는 async/.await에 대해 간단히 살펴 보았고 간단한 서버를 구축하는데 사용했습니다. 이 장에서는 async/.await에 대해 일반론 적인 설명과 작동 방식 및 비동기 코드와 전통적인 Rust 프로그램 과의 차이점에 대해 자세히 설명합니다.

async/.await는 Rust 구문의 특별한 부분으로 block하지 않고 현재 스레드의 제어를 내어 놓아서 작업이 완료되기를 기다리는 동안 다른 코드가 진행될 수 있게 해줍니다.

async를 사용하는 두 가지 주요 방법이 있습니다 : async fnasync 블록. 각각은 Future trait을 구현하는 값을 반환합니다 :


// `foo()` returns a type that implements `Future<Output = u8>`.
// `foo().await` will result in a value of type `u8`.
async fn foo() -> u8 { 5 }

fn bar() -> impl Future<Output = u8> {
    // This `async` block results in a type that implements
    // `Future<Output = u8>`.
    async {
        let x: u8 = foo().await;
        x + 5
    }
}

첫 장에서 보았듯이 async 본문과 다른 futures는 게으릅니다. 그들은 execute 될 때까지 아무 것도 하지 않습니다. Future를 실행하는 가장 일반적인 방법은 .await 입니다. .awaitFuture 에서 호출되면 완료까지 실행을 시도합니다. Future가 block되면 제어권을 내어 놓아 다른 future가 제어를 얻을 수 있습니다. 더 많은 진전이 가능할 때 해당 Future가 executor에 의해 선택됩니다. Executor에 의해 실행을 재개하여 .await가 완결 되도록 합니다.

async 생명주기

전통적인 함수와 달리, reference 또는 non-static인 인수를 취하는 async fn는 생명주기가 인수의 생명주기에 묶여 있는 Future를 반환합니다 :

// This function:
async fn foo(x: &u8) -> u8 { *x }

// Is equivalent to this function:
fn foo_expanded<'a>(x: &'a u8) -> impl Future<Output = u8> + 'a {
    async move { *x }
}

이는 async fn에서 반환 된 future가 정적이 아닌 인수가 여전히 유효한 동안에 .await 되여야 함을 의미합니다. 함수를 호출 한 직후에 future를 기다리는 .await의 경우 (foo(&x).await 와 같이) 이것은 문제가 되지 않습니다. 그러나 future를 저장하거나 다른 작업이나 스레드로 전송하면 문제가 될 수 있습니다.

reference를 인수로 갖는 async fnstatic future로 설정하는 일반적인 해결 방법은 async 블록 안에서 async fn 을 호출하면서 인수를 함께 엮어 주는 것입니다 :

fn bad() -> impl Future<Output = u8> {
    let x = 5;
    borrow_x(&x) // ERROR: `x` does not live long enough
}

fn good() -> impl Future<Output = u8> {
    async {
        let x = 5;
        borrow_x(&x).await
    }
}

인수를 async 블록으로 옮기면 수명이 연장되어 good에 대한 호출에서 리턴된 Future의 수명과 일치 되었습니다.

async move

async 블록과 클로저는 보통의 클로져와 마찬가지로 move 키워드를 허용합니다. async move 블록은 변수가 참조하는 데이터의 소유권을 갖고 , 현재 scope보다 오래 생염이 유지되도록 허용해 주지만 해당 변수를 다른 코드와 공유하는 기능을 포기합니다.

/// `async` block:
///
/// Multiple different `async` blocks can access the same local variable
/// so long as they're executed within the variable's scope
async fn blocks() {
    let my_string = "foo".to_string();

    let future_one = async {
        // ...
        println!("{}", my_string);
    };

    let future_two = async {
        // ...
        println!("{}", my_string);
    };

    // Run both futures to completion, printing "foo" twice:
    let ((), ()) = futures::join!(future_one, future_two);
}

/// `async move` block:
///
/// Only one `async move` block can access the same captured variable, since
/// captures are moved into the `Future` generated by the `async move` block.
/// However, this allows the `Future` to outlive the original scope of the
/// variable:
fn move_block() -> impl Future<Output = ()> {
    let my_string = "foo".to_string();
    async move {
        // ...
        println!("{}", my_string);
    }
}

멀티 스레드 Executor 에서의 .await

멀티 스레드 Future executor를 사용할 때 Future가 스레드 간에 이동 할 수 있습니다. 따라서 비동기 바디에 사용 된 모든 변수는 스레드 사이에 이동 가능 해야 합니다. .await는 잠재적으로 새로운 스레드로 전환 되는 결과를 가질 수 있습니다.

이것은 Rc, &RefCell 또는 다른 Send trait을 구현하지 않는 type을 사용하는 것이 안전하지 않다는 것을 의미합니다. 이 것은 Sync를 구현하지 않은 type을 사용하는 것에도 해당됩니다.

(주의: 이 type이 .await를 호출하는 동안의 scope 내에 있지 않는한 이 type 들을 사용할 수 있습니다.)

마찬가지로, future를 인식하지 못하는 전통적인 lock을 .await를 가로 질러 유지하는 것은 좋지 않습니다. 왜냐하면 스레드 풀이 잠길 수 있기 때문입니다 : 하나의 작업이 .await lock을 갖고 있다가 executor에 제어를 양보하여 다른 작업을 수행 할 수 있는데 그 다른 작업이 lock하려고 하면 교착 상태를 유발합니다. 이것을 피하려면 std::sync에 있는 것이 아니라 futures::lock에 있는 Mutex를 사용하십시오.