• Rust

Async Rust

Rust 언어의 비동기 프로그래밍 인프라 간단히 살펴보기

2019. 3. 27

비동기 프로그래밍으로 최적의 성능을 쉽게 달성할 수 있다.

Futures

GitHub - rust-lang-nursery/futures-rs: Zero-cost asynchronous programming in Rust

Future는 미래에 얻을 값의 프록시이다.

Poll

Rust Future의 핵심에는 poll() 메서드가 있다. poll() 메서드는 Async::Ready(val), Async::NotReady 반환한다. 이건 자신의 상태를 반환하는 것인데 폴링하다 보면 Async::Ready 반환하며 작업을 완료할 수 있다. Async::Ready 를 반환한 Future에 또 poll을 하면 안된다.

Rust의 Future는 첫 번째 폴링이 발생하기 전까지는 아무것도 실행하지 않는다.

poll() 이라고 해서 완전히 tight loop 을 도는 것은 아니다.

  • epoll(linux) 이 이벤트 루프를 block 할 수 있다.
  • mio는 epoll, kqueue 등 nonblocking IO 인프라 제공한다. 직접 쓸 일은 없다.

Executor

Future는 Executor와 긴밀하게 협력하며 작동한다.

poll()이 지켜야 하는 contract가 있다. NotReady 를 반환할 때 Executor 에게 자신을 다시 스케줄링 해 달라고 부탁해 둬야 한다는 것이다. 이 contract를 어기면 Future가 더 진전되지 않는다.

// https://rust-lang-nursery.github.io/futures-api-docs/0.3.0-alpha.13/futures/prelude/trait.Future.html

fn poll(self: Pin<&mut Self>, waker: &Waker) -> Poll<Self::Output>

// 0.1 버전 Future
// https://docs.rs/futures/0.1.25/futures/future/trait.Future.html
fn poll(&mut self) -> Poll<Self::Item, Self::Error>

리스케줄링을 위한 핸들이 waker다.

예전 futures 구현은 poll이 waker를 받지 않는 것을 볼 수 있다. 이때는 thread local 변수로 waker를 지정하고 글로벌로 사용했었는데 이제는 명시적으로 함수 호출시 전달받는다.

Pin

fn poll(self: Pin<&mut Self>, waker: &Waker) -> Poll<Self::Output>

Pin은 메모리 위치에 관한 것이다. Async 구현에 필요하다.

  • Unpin을 구현하거나,
  • 한 번 Pin 된 이후로 움직이지(move) 않으면

Pin이다.

관계가 좀 복잡하다. 아래는 무슨 뜻일까?

impl !Unpin for Foo {}

Foo가 Unpin이 안 된다는 (impl !) 이야기이므로, &Foo fooref가 Pin이려면 fooref의 타겟이 움직여서는 안된다는 것이다.

Unpin이 구현되어 있다면 그 타입은 자유롭게 움직여도 Pin 이다. 대부분은 Unpin이다. i32, Vec, Box 모두 이동해도 포인터 정합성에 문제가 발생하지 않는다.

내부 참조에서 문제가 발생한다. 이런 식이다.

struct Bar {
  els: Vec<i32>,
  some_of_els: &[i32]
}

some_of_els가 els의 슬라이스일 때, some_of_els가 정해진 이후로 Bar 값이 움직이게 되면 some_of_els 는 유효하지 않은 포인터가 된다. 어떤 구조체의 위치(메모리상)를 그 구조체 자신에게 적어둔다면 구조체가 메모리상에서 움직였을 때 위치 값은 더 이상 유효하지 않은 것으로 이해할 수 있다.

이런 상황의 Bar가 !Unpin 이 된다.

보통 ownership 제약에 의해 위와 같은 일은 발생하지 않는데 (값을 빌려준 상태에서 움직일 수 없다, 또는 움직인 값을 빌릴 수 없다), async/await 구현에서는 필요하다.

struct Bar<'a> {
  els: Vec<i32>,
  some_of_els: &'a [i32]
}

fn move_bar(bar: Bar) {}

fn main() {
    let inner = vec!{3,1,2,3};
    let temp = vec!{3,2};

    let mut bar = Bar{
        some_of_els: &temp[2..3],
        els: inner,
    };
    bar.some_of_els = &bar.els[2..3];
    move_bar(bar); // error: cannot move out of `bar` because it is borrowed
}

async/await

async/await 문법은 RFC2394에서 소개되었다. 가급적 Future 자체를 덜 신경쓰면서 비동기 프로그래밍을 할 수 있게 해준다.

async fn function(argument: &str) -> usize {
     // ...
}

async 함수는 호출되었을 때 즉각 함수 바디를 실행하지 않고 Future를 구현한 익명의 타입을 반환한다. Future가 poll 되면 다음 await 까지 실행한다.

await된 부분은 컴파일러에 의해 대략 아래처럼 펼쳐진다.

let mut future = IntoFuture::into_future($expression);
let mut pin = unsafe { Pin::new_unchecked(&mut future) };
loop {
    match Future::poll(Pin::borrow(&mut pin), &mut ctx) {
          Poll::Ready(item) => break item,
          Poll::Pending     => yield,
    }
}
// https://github.com/rust-lang/rfcs/blob/master/text/2394-async_await.md#the-expansion-of-await

하지만 yield 개념을 async 외부에서 표현할 수 없으므로, await 구문과 정확히 동치인 Rust 코드를 보이는 것은 불가능하다.

asyncasync fn(함수), async {}(클로저) 처럼 사용할 수 있지만 await문법은 어떻게 해야 하는지 활발한 토론이 진행중이다.

추후 .await 형태의 후치 오퍼레이터로 결정되었다.