RustでI/Oを扱うプログラムを書く機会がありました。非同期I/Oのほうがパフォーマンスがよくなるらしく、tokio というフレームワークがよく使われているとのこと。tokio では Future をベースとして非同期処理を書くようです。明るい Future を生み出していけばよいプログラムがかけそうですね 。しかし、 tokioやFutureがなんもわからんという問題がありました。そこで、JavaScriptのFuture、Promiseと対比させてRustのFutureについてまとめます。
JavaScriptのPromise
昔のJavaScript(Node.js)では、ファイル読み込みやネットワークアクセス等のI/O待ちが発生するときはコールバックという仕組みを用いていました。人々は辛くなり、ES2015ではPromiseが導入されました。さらにES2017ではasync/awaitが導入され、一層I/Oの処理が書きやすくなりました。
ではまずはJavaScriptで簡単なPromiseを書いてみましょう。
const promise = new Promise((resolve, reject) => {
console.log("promise called");
resolve('success');
});
promise
の中身のコードはPromiseを定義した時点でランタイムにより実行されはじめます。そのため、上記のコードを実行するとすぐに promise called
と表示されます。
Promise が成功したときの処理は then
、失敗したときの処理は catch
でつなげます。50%の確率で成功、50%の確率で失敗する promise を作ってみましょう。
const promise = new Promise((resolve, reject) => {
console.log("promise called");
if (Math.random() * 10 < 5) {
resolve('success');
} else {
reject('failure');
}
});
promise
.then(res => console.log(res))
.catch(err => console.error(err));
RustのFuture
JavaScriptのPromiseに対応するオブジェクトはFutureです。
Promise内では、処理が完了したときに resolve
もしくはreject
を呼び結果を通知することになっています。一方で、Futureはポーリングモデルを採用しており、tokio
等の非同期ランタイムがFutureの結果を取得しにいきます。tokio
はFutureのpoll
メソッドを定期的に呼び出します。poll
はまだ実行中だったら Poll::Pending
を、処理が完了したら Poll::Ready
を返します。
Futureは futures クレート1のものを使います。現時点(2019/08/14)では stable 版の Rust で利用できる futures クレートの最新版は 0.1.28 です。
futures バージョン 0.1 における Future は次のようなシグネチャをもちます1
pub trait Future {
type Item;
type Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error>;
}
では Promise で書いた処理を Rust で書いてみましょう。 poll
メソッドを実装すれば OK です。 50% の確率で Future が正常終了し "success" が出力され、 50% の確率で Future がエラー "failure" を返します。
struct MyFuture;
impl Future for MyFuture {
type Item = String;
type Error = String;
fn poll(&mut self) -> Poll<String, String> {
println!("poll called");
let mut rng = rand::thread_rng();
let i: i32 = rng.gen_range(0, 10);
if i < 5 {
Ok(Async::Ready("success".to_string()))
} else {
Err("failure".to_string())
}
}
}
Rust の Future を実行するには、 Future を executor にわたす必要があります。 futures クレートにも executor があるので、とりあえずこちらを使ってみましょう。
Promise の then は and_then
に、 catch は map_err
に対応します。 executor に突っ込む future は Item = (), Error = ()
にする必要があるため、 Ok(())
や ()
を返して型を合わせます。
fn main() {
let future = (MyFuture{}).and_then(|res| {
println!("{}", res);
Ok(())
}).map_err(|err| {
println!("{}", err); // () を返す
});
// futures クレートの executor
futures::executor::spawn(future).wait_future().unwrap();
println!("finished");
}
これでも動きますが、wait_future は Future の実行が完了するまでスレッドをブロックしてしまうので、通常は tokio などのランタイムを使います。
impl Future for MyFuture {
...
}
fn main() {
let future = (MyFuture{}).and_then(|res| {
println!("{}", res);
Ok(())
}).map_err(|err| {
println!("{}", err); // () を返す
});
tokio::run(future);
println!("finished");
}
RustのFutureのFuture
Rust においても async/await が使えるようにするため、 Rust 1.37 で Future が std::future に収録されました。さらに、Rust 1.38 では async/await が stable になる予定です。近い将来には、 std::future の Future が広く使われるようになるでしょう。 tokio も master ブランチでは std::future をデフォルトで使うように開発が進められているようです。
futures 0.1 の Future と futures 0.3 の Future は相互変換できるため、環境を移行するのも難しくはないと思います(希望をこめて)。
futures 0.3 や std::future における Future は次のようなシグネチャを持ちます。
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}
std::future では成功/失敗の概念を Future から切り離しました。そのため、成功/失敗を表現するためには、Result を返すようにする必要があります。では、さきほどの処理を std::future の Future で書き換えてみましょう。
struct MyFuture;
impl Future for MyFuture {
type Output = Result<String, String>;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
println!("poll called");
let mut rng = rand::thread_rng();
let i: i32 = rng.gen_range(0, 10);
if i < 5 {
Poll::Ready(Ok("success".to_string()))
} else {
Poll::Ready(Err("failure".to_string()))
}
}
}
この Future を実行するには、 Future を executor にわたす必要があります。 ひとまずは futures::executor::block_on を使ってみましょう。and_then
はもとの Future と同じ型を返す必要がありますが、 map_ok
ではもとの Future と違う型を返すことができます。
let future = (MyFuture{}).map_ok(|res| {
println!("{}", res);
}).map_err(|err| {
println!("{}", err);
});
let _ = block_on(future);
futures 0.3 では ThreadPool
という executor もあります。ThreadPool
は複数のスレッドを用いて Future を実行してくれるため、通常はこちらの executor を使ったほうがいいと思います。いずれは tokio 等のランタイムでも気軽に std::future も使えるようになるでしょう。
fn get_future() -> impl Future<Output=Result<(), ()>> {
(MyFuture{}).map_ok(|res| {
println!("{}", res);
}).map_err(|err| {
println!("{}", err);
})
}
let future = get_future();
ThreadPool::new().expect("Failed to create threadpool").run(future);
Future の途中結果を print したいことは多々あると思います。このために、 inspect_ok
や inspect_err
が提供されています2。 print するだけだったら map ではなくて inspect を使ったほうがいいでしょう。
let future = (MyFuture{}).inspect_ok(|res| {
println!("{}", res);
}).inspect_err(|err| {
println!("{}", err);
});
let _ = block_on(future);
ソースコードの全文は Github にあります。
ドキュメントについて
Rust に async/await を取り入れるためには、Future を言語のコア機能に取り入れる必要がありました。一方で、言語のコア機能に Future を取り入れてしまうと、 Future の変更が困難になってしまいます。そのため、 std::future::Future の機能は最小限になっています。
https://doc.rust-lang.org/std/future/index.html
Future の便利機能は今後も futures クレートで提供されるようです。std::future と互換性がある futures 0.3 のドキュメントを参照してください。
https://rust-lang-nursery.github.io/futures-api-docs/0.3.0-alpha.18/futures/index.html
then
等の便利メソッドは FutureExt トレイトで提供されます。これは use futures::FutureExt
すると使えるようになります。
type Output = Result<String, String>
のように Future の値を Result
型にした場合、 use TryFuture
とすると TryFuture トレイト の機能が利用できるようになります。
また、 use futures::TryFutureExt
すると、 TryFutureExt トレイト の機能が使えるようになります。map_ok
や inspect_err
はこちらで定義されています。
まとめ
この記事では JavaScript の Promise と対比させて、Rust の Future について解説しました。 Rust の Future はポーリングモデルを採用し、ランタイムが poll
を呼び出すことで処理が進んでいきます。
Future には 0.1 系と 0.3 系があり、近い将来は 0.3 系が広く使われていくかと思いますが、これらは相互変換できます。現状は tokio 等のランタイムがデフォルトでサポートしており、ドキュメントの多い 0.1 系を使っていくのがいいでしょう。
ツッコミ、感想等コメントお待ちしています
謝辞
この記事の一部は @__pandaman64__ 氏に助言をいただき執筆しました。この場を借りて謝意を表します。