- Rust
Async Rust
Rust 언어의 비동기 프로그래밍 인프라 간단히 살펴보기
비동기 프로그래밍으로 최적의 성능을 쉽게 달성할 수 있다.
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 코드를 보이는 것은 불가능하다.
async
는 async fn
(함수), async {}
(클로저) 처럼 사용할 수 있지만 await
문법은 어떻게 해야 하는지 활발한 토론이 진행중이다.
추후
.await
형태의 후치 오퍼레이터로 결정되었다.