RustのFuturesがわからん

ぽけーっとしていたら2019年も半ば、なんと今月末にRust 1.36がstableになるらしい。何が特別かってついにfuturesがstable入りしてしまうのです!

futuresわからんよね問題

で、本題なんですが、futures、わからない。いやまあわからないって言ったってそりゃリファレンス、例えばこれとかこれ見れば使えると思いますよ?そういう問題ではなく、「一体何やってるかわからん」という問題なのです。

Pollベースの非同期計算

非同期計算でもっとも有名なものといえばJavascriptのPromise だと思います。もちろん、ネイティブな実装がどうなっているかは別として、自分で勝手なPromiseを作ろうと思ったらたとえば

new Promise(resolve => {
    console.log('hoge');
    // Some time-consuming calculation
    resolve();
})

みたいな感じにするでしょうか。つまりJavascriptのPromiseというのは、ざっくり言って「(threadはないですが)threadのラッパー」のようなイメージで、逐次実行していくのだろうなあと思えるわけです。ところがRustのFuture

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}

こんなシグネチャで、しかもpollは基本的に「呼ばれたらすぐにPoll<Self::Output>を返さなければならない(This method does not block if the value is not ready.)」とされています。さらに読み進めると「Instead, the current task is scheduled to be woken up when it’s possible to make further progress by polling again. The wake up is performed using the waker argument of the poll() method, which is a handle for waking up the current task.(『タスク』はポールした結果何らかの進捗があった場合に再開されるようスケジュールされる。pollメソッドのwaker引数を介して再開される。)」とあります。『タスク』って何?スレッドと違うの?そもそも誰がpollを呼ぶの?と、謎が深まるわけです。しかも悪いことにfutures 0.1では再開をスケジュールするメソッドがfutures::task::current().notify()であり、これはここらへんで定義されたthread local変数を呼んだりなどしているわけですが、まわりまわってこれもわけのわからなさを増していました。どうやって起こせばいいんだ!って感じです(おそらく結局tokioなどの実装依存です)。Rustなのにポインタを駆使している、なかなか業の深そうなコードです。これは歴史のあるコードだからでしょうか?

ここからもわかるように、「Rustの非同期計算がわからん」理由というのは、RustのFutureがthreadベースというよりpollingベースになっていて、誰が何をどのように実行するのかがぱっとわからないところにあるように思います。さらにまあそれがわかったとして、ナイーブなthreadベースの計算の方が簡単そうに思えるのになんでまたそんなことをするのだろうかという疑問もわいてきます。というかぼくはそれがわからんなあという話です。

Futureはただの「インターフェース」

というようにfuturesの概念が全く分かっていなかったのですが、これとかこれみたいな素晴らしいドキュメントをみつけたり、自分でもFutureを使ったプログラムを書いていて、なんとなくfuturesを理解してきました。特に大事だと思ったのは、「Futureは結局のところ非同期計算の『ありかた』を定義したインターフェースに過ぎない」ということです。(あえてインターフェースと呼びます)

とりあえず、Futureが内部的にどういう実装で非同期計算をするのかということを完全に忘れて、Futureのsignatureを見てみましょう。 このversionのリファレンスによると、Futuremapjoin, and_thenで次々とチェインしていけるということがわかります。というか、そもそも上のpollメソッドを認めれば、これらを複数個保持するような構造体を作ることで、簡単にFutureを束ねられることがわかります(つまりFutureはモナドであるわけです)。つまり、Futureは計算の進捗を状態として保持する構造を抽象的にしたtraitであり、計算が進むごとに状態が変化する有限オートマトンに他ならないということがわかります。

さて、ここからわかることとしてーもちろん本当は逆ですがー計算の進捗は有限オートマトンでモデリングされるということがあります。これは同期的か非同期的かということにはかかわりないのですが、逆に言うと有限オートマトンで計算の依存関係を示しておけば、それで許される範囲内であとは計算を非同期的に実行できるわけです。つまり、有限オートマトンは非同期計算をするうえで非常に都合の良い道具です。

別の言い方もできます。例えばexplicitにthreadを使って依存関係のある計算を非同期に行おうと思ってコードを書こうとすると、結局有限オートマトンlikeな構造でないと書きづらくなります。たとえばPythonのconcurrency.futureというモジュールはなんだかFuturesに似たようなモジュールですが、pollに相当するメソッドはブロッキングです。その結果として、「前の計算結果を利用して非同期な計算を行う」ということはできなくて、とにかくセットで一連の非同期計算をすべて一気に(同一プロセスで)やらなくてはいけません。あるいは生のthreadを使って非同期計算をやることもできますが、依存関係を入れようと思ったら有限オートマトン的な構造にするしか思いつきません。 つまり、FutureはRustで非同期計算をやるにあたってのある種の取り決め、インターフェースなわけです。

Futureがインターフェースに過ぎないということは、それ以上のこと、すなわち「誰が何をどのように実行するのか」ということはFutureをimplementするstruct自体が自分で決めなくてはならない、ということと表裏一体です。 たとえば、futures関連で最も有名なフレームワークはtokioといいます。tokioを使った場合、上の「誰が何をどのように実行するのか」という問いに対する答えは、「tokioがどうにかして実行する」ということになります。しかし、実際、こんなサイトがありますが、tokio以外にもFutureを実行できるライブラリ(reactor)は存在するわけです。ですから、結局のところそもそも一番最初に挙げた「Futureを誰がどのように実行するのか」というのは良い問いではなく、「tokioFutureをどう実行しているのか」あるいはもっと具体的に「tokioを使えばどのようなFutureが作れるのか」ということを疑問に思うべきなのです。(tokio insideはそのうちまたしらべて記事を書きたいですね)

Future VS coroutine (green threading)

一方、「計算の進捗を状態として保存する」という意味では、coroutineもまさにそのようなオブジェクトです。coroutineではyieldのような、特殊な「メソッド」を呼ぶことで、スタックフレームやipを含むレジスタをどこかの領域に退避させ、再度続きから実行することができます。coroutineを見かけ上並列に実行するのがgreen threadingで、I/O waitなどの「重い」処理をトリガにスタックフレームとレジスタを退避させて、その処理が終わるまでは対象のcoroutineを実行しないようにしています。そして処理が終わったら、グリーンスレッドを「実行可能とマーク」してgreen threadを処理するスレッドを起こせばよいわけです。 この場合、計算の依存関係はメソッドチェインではなくcorountine内のコードとしてimplicitにステートマシンを記述できます。このようなgreen threadingも、しばしば並行処理のフレームワークとして使われます。(golangとかそうらしいですね。詳しく知りませんが…。)

本質的には、green threadingもfuturesもやっていることは全く同じといえるでしょう。「計算の進捗を状態として保存」して計算を行うわけです。違いはコードの書き方と実行コストで、green threading(coroutine)は普段のプログラムとほとんど同じようなコードが書ける一方、コンテキストスイッチングのコストが必要である。一方、futuresは、モナディックなメソッドチェインを使った書き方をしなければならないが、コンテキストスイッチングは不要である、というわけです。

futuresまわりで有名なドキュメントに、Zero-cost futures in Rustがあります。最初読んだ時、思いました。「Zero-costって何?いや生のthreadのがコスト少ないよね?」しかし、上で議論したように、依存性を持った計算を実行しようと思ったら、どうしてもステートマシン的構造は避けられそうにありません。そうすると、green threadingに比べればモナディック非同期計算はまあcost-freeかな、という気がしてきます。そう思って上のドキュメントをながめると、今更腑に落ちたのでした(実際は生のthreadでのデータレースとかも話しているが、まあそれは説得力が薄い気がしていたし今もする)。

まとめ

Comments

comments powered by Disqus