async
/.await
첫 장에서 우리는 async
/.await
을 짧게 다뤘습니다. 이제 async
코드가
어떻게 동작하고, 어떻게 전형적인 러스트 프로그램과 다른지 더 자세히 들여다봅시다.
async
/.await
는 현재 실행중인 스레드를 블로킹하지 않고 제어권을 놓아줄 수
있게 만들어주는 특별한 러스트 문법입니다. 이를 통해 어떤 명령이 완성될 때까지
기다리면서(역주 : 예를 들어, 소켓에 데이터가 들어오기를 기다린다던가) 다른
코드들이 실행될 수 있습니다.
async
를 다루는 주 방법에는 두 가지, async fn
과 async
블록이 있습니다. 각각
Future
트레잇을 구현한 값을 반환합니다.
// `foo()`는 `Future<Output = u8>`을 구현한 타입을 반환합니다.
// `foo().await`의 결과는 `u8` 타입의 값입니다.
async fn foo() -> u8 { 5 }
fn bar() -> impl Future<Output = u8> {
// 이 `async` 블록은`Future<Output = u8>`을 구현한
// 타입을 반환합니다.
async {
let x: u8 = foo().await;
x + 5
}
}
첫 장에서 봤듯이, async
안쪽과 그 밖의 future 구현체는 게으릅니다. 즉 실행될
때까지 아무 일도 안 합니다. Future
를 실행하는 통상적인 방법은 Future
에
.await
를 사용하는 것입니다. Future
가 .await
되면, 그 Future
가 완료될
때까지 실행하려고 할 것입니다. 하지만, Future
가 실행 도중에 블록되면 현재
스레드의 제어를 놓아줍니다. 그리고 Future
가 더 진행이 될 수 있는 상황이
돌아오면, Future
는 실행자에게 선택돼 실행을 재개할 것이고, 따라서 .await
이
완료될 수도 있을 것입니다.(역주 : 물론 Future
가 다시 블록될 수도 있다)
async
한 수명
참조나 기타 'static
하지 않은 인자를 취하는 async fn
은 다른 전형적인 함수들과
달리 인자의 수명에 묶인(bound) Future
를 반환합니다.
// 이 함수는
async fn foo(x: &u8) -> u8 { *x }
// 이 함수와 같습니다.
fn foo_expanded<'a>(x: &'a u8) -> impl Future<Output = u8> + 'a {
async move { *x }
}
즉, async fn
에서 반환된 future는 자신이 받은 'static
이 아닌 인자가 유효한
상태에서만(역주 : 즉, 참조의 수명 범위내에서만) .await
되어야 합니다. 보통
foo(&x).await
와 같이 함수를 호출하고 바로 future를 .await
할 때는 문제가
없습니다. 허나 future를 또다른 작업이나 스레드로 저장하거나 보내면 문제가 생길
수 있습니다.
참조를 인자로 가진 async fn
을 'static
한 future로 바꾸는 유용한 방법은 바로
async fn
의 호출과 인자를 하나의 async
블록 안에 두는 것입니다.
fn bad() -> impl Future<Output = u8> {
let x = 5;
borrow_x(&x) // ERROR: `x`의 수명이 충분하지 않습니다.
}
fn good() -> impl Future<Output = u8> {
async {
let x = 5;
borrow_x(&x).await
}
}
인자를 async
블록 안으로 옮기면, 그 인자의 수명을 늘리기 때문에 good
을
호출하여 반환되는 Future
의 수명과 일치시킬 수 있습니다.
async move
async
블록과 클로저는 보통 클로저처럼 move
키워드를 허용합니다. async move
블록은 자신이 참조하는 변수의 소유권을 획득하면서, 그 변수를 원래의 스코프
바깥에서도 유효하게 만들 것이지만, 다른 코드는 그 변수를 사용할 수 없게 됩니다.
/// `async` 블록:
///
/// 서로 다른 여러 개의 `async` 블록들은, 어떤 지역 변수의 스코프 안에서
/// 실행되는 한, 그 지역 변수에 함께 접근할 수 있습니다.
async fn blocks() {
let my_string = "foo".to_string();
let future_one = async {
// ...
println!("{my_string}");
};
let future_two = async {
// ...
println!("{my_string}");
};
// 두 future를 완전히 실행해 "foo"를 두 번 출력합니다:
let ((), ()) = futures::join!(future_one, future_two);
}
/// `async move` 블록:
///
/// 오직 한 개의 `async move` 블록만 (역주: 그 `async move` 블록이) 캡쳐한
/// 변수에 접근할 수 있습니다. 왜냐하면, 캡쳐된 변수는 `async move` 블록이
/// 생성한 `Future`의 안으로 이동하기 때문입니다.
///
/// 반면에, 이렇게 함으로써 `Future`가 그 변수의 원래 스코프 밖에서도 실행될 수
/// 있게 됩니다.
fn move_block() -> impl Future<Output = ()> {
let my_string = "foo".to_string();
async move {
// ...
println!("{my_string}");
}
}
멀티스레드 실행자 상에서 .await
하기
멀티스레드 용 Future
실행자를 사용하면, 모든 .await
은 종종 새로운 스레드에서
Future
를 실행시킬 수도 있습니다. 따라서 Future
가 스레드를 갈아타는 상황이
생길 것이고, 당연히 async
블록 안쪽에서 쓰인 모든 변수도 반드시 스레드를
갈아탈 수 있어야 합니다.
그 말인 즉슨 Sync
트레잇을 구현하지 않는 타입에 대한 참조를 포함, Send
트레잇을 구현하지 않는 Rc
, &RefCell
등 어떠한 타입도 안전하게 사용할 수
없다는 뜻입니다.
(주의: 이러한 타입들이 .await
을 호출하는 스코프 안에 존재하지 않는다면,
사용할 수 있긴 있습니다.)
비슷하게, future를 고려하지 않고 만들어진 전통적인 락(lock)을 .await
을 사이에
두고 사용하는 것은 좋지 않습니다. 왜냐하면 스레드풀을 잠궈버릴 수도 있기
때문입니다. 예를 들어, 한 태스크가 락 하나를 가져온 상태에서 .await
하고
실행자에게 (역주: 현재 실행중인 스레드를) 내어준 경우, 다른 태스크가 다시 그
락을 가지려고 하면 데드락이 발생합니다. 이런 상황을 방지하기 위해, 이런 문제를
방지하기 위해 std::sync
말고 futures::lock
에 있는 Mutex
를 사용하시기
바랍니다.