yieldとコルーチンと非同期処理

1人AdventのDay-15です。もう2週間経ったのか、12月よ・・

adventar.org

最近、ちらちらっと「Swoole」という単語を聞くようになった気がしており、それについて調べてみよう〜という予定でした。が、「まずコルーチンとかについて頭の中を整理したいな・・」と思ったので、このようなタイトルになっております。

きっかけ

Swooleについて初めて意識したのが、TLにこのツイートが流れてきたときかなあーと思います・・

Swooleってなんなのさ

BEAR.Sundayのサイトに載っている説明はシンプルにまとまっていました。

SwooleとはC/C++で書かれたPHP拡張の1つで、イベント駆動の非同期&コルーチンベースの並行処理ネットワーキング通信エンジンです。 Swooleを使ってコマンドラインから直接BEAR.Sundayウエブアプリケーションを実行することができます。パフォーマンスが大幅に向上します。

bearsunday.github.io

ググったら「副題: 最近Swooleが楽しい話」というUzullaさんの記事が出てきました。

最後まで読んでみて、いろいろと「なるほど」がありました。結構ボリュームがありますね。 スライドを見ていたら、サイトも出てきました。

www.swoole.co.uk

「プロダクション品質の非同期PHPプログラミングフレームワーク」ふむ。

・・・なんだか「こんにちは、闇の方から来ました。私、色々できます」みたいな凄みを感じます。

Sooleと非同期プログラミング

私が「Swoole気になるな」と思ったのは、「非同期」があるの?と思ったのが強いきっかけの1つです。
非同期、よいですね、ふふふ。

先のuzullaさんのスライドを見ると「Swoole、めっちゃ機能多そうだな」という感想をいだきます。何か新しい仕組みや概念を理解しようとするときに、私は「なにか1つポイントを見つけて、それを軸としてイメージを具体化していく 」というアプローチを好みます。それをしながら、「 必要になったら時点で他の筋に移動したり、行ったり戻ったりする」を繰り返して、全体像を掴んでいこうという感じです。

多機能だからこの記事で全部触れるの無理よな〜っていう思いもあり、まず「非同期プログラミング」をキーワードとして調べている内に、主題が入れ替わった形です。

PHPとコルーチン

先のスライドの中にも、「コルーチン」が出てきますね。

[f:id:o0h:20181216010344p:plain f:id:o0h:20181216010434p:plain

PHPでの非同期処理を得意にさせるrecoilなどにもcoroutineについて言及があります。

では、コルーチンとは一体なにで、どうしてそれが非同期処理と関係があるのでしょう?

そもそも「コルーチン」がなにか、という話をつまみ食いしておきます。

まずは、例によってWikipediaです。

コルーチンはいったん処理を中断した後、続きから処理を再開できる

とか

接頭辞 co は協調を意味するが、複数のコルーチンが中断・継続により協調動作を行うことによる。

(https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%AB%E3%83%BC%E3%83%81%E3%83%B3)

なんて書かれています。
「継続」「中断」「再開」という言葉が出てきました。これらを意識することができれば、意味がつかみやすくなるのかもしれません。

継続

継続については、こちらの記事が良かった。
なんでも継続

「意識することができれば」と書きましたが、「今までもそこにあったのに、見えてなかった/認識することができなかった対象を、ふと「当たり前」に存在を感じられるようになれば」ということで、この書き方をしました。そして、この「なんでも継続」の記事が、正にそのスタンスで書かれていて良かったな〜と。。

すんごいザックリといって「なんかの処理をしていて、途中でどっか行って、戻ってきたらその 続き から処理を行う」というのを強調するために「継続」という概念が必要な気がします。「どっか行って」=中断、「戻ってきて」=再開。

関連: 制御構造 - Wikipedia

yield

さて、PHPにおいても「中断」「再開」といった単語を(サブルーチンのそれとは違い)強く意識する場面があるのではないでしょうか。
イテレータ、ジェネレータ、yieldといったキーワードを思い出してください。

ジェネレータ(Generator)とは、イテレータコンパチなインターフェイスを持つけど、 指すべきコレクションがあるわけでもなく、そのたんびに 値を作り出して返すようなモノを作るモノをいいます。

ジェネレータ と コルーチン - みねこあ

PHPはyieldによってジェネレータを作成できます。

  • 中断:
    • ルーチンの内部の任意の箇所で、(呼び出し側に値を返して)処理を終わらせ
  • 再開:
    • 「さっきやったとこ」の続きから処理を始める

という内容を備えています。「一旦処理を中断した後、続きから処理を再開できる」ということであれば、コルーチンの定義に当てはまると言って良いのでしょうか?

ジェネレータ/コルーチン

yield(PHP)はジェネレータ関数を作るのだから、じゃあyeildを使えばコルーチン?コルーチン = ジェネレータ?と思い、今度は「ジェネレータ」についてWikipediaでみてみます。

ジェネレータの実装としてはコルーチンやcall/ccやマルチスレッドを使う方法が考えられる。

ジェネレータ (プログラミング) - Wikipedia)

ということで、どうも一致する概念ではないようです。
どういう差なのかな、と調べてみたらPythonに関しての例を説明している記事に行き当たりました。

yieldした後にコントロール出来るという点がジェネレータと違うようです。

Pythonのyield(コルーチン編) | KISO-REN

この「コントロール可能か否か」というところで、コルーチンは分類されるぞ!というような事をStackoverlowの問答で発見しました。

In the asymmetric coroutine model, "asymmetry" refers to the fact that there is a stack-like caller–callee relationship between coroutines. A coroutine can either call another coroutine or suspend itself by yielding control to its caller, typically also yielding a value to the caller at the same time.

What is the difference between asymmetric and symmetric coroutines? - Stack Overflow

ジェネレータを含む「非対称コルーチン」においては、「一旦処理に入ったら、そのまま、最後まで内部生成された値を返す」ということになり、これを「一方的(= callerには制御権がなく、コルーチンにのみ自身の制御権がある)」と言っている・・のでしょうか?そんな感じと理解しました。

そしてさらに、Pythonの例を見つけました。

魅力的なPython: ジェネレーターによるステート・マシン

かなり前のバージョンにおける言及をしている記事で、この中では「(Python2.2で導入された)yieldでのジェネレータは半コルーチンで、本当のコルーチンを実装したよ」とされています。

内容を見てみると、「ジェネレータの中から別のジェネレータを呼ぶ事は可能」というのを応用して、「ジェネレーター同士がお互いを呼び合う」ようにしている・・というのがポイントでしょうか。

先の「Pythonのyield(コルーチン編) | KISO-REN」の記事で言及されているPython Coroutineの元ネタPEPを見てみると、次のように書かれています。

However, if it were possible to pass values or exceptions into a generator at the point where it was suspended, a simple co-routine scheduler or trampoline function would let coroutines call each other without blocking -- a tremendous boon for asynchronous applications.

PEP 342 -- Coroutines via Enhanced Generators | Python.org

ということで、IBMの記事にて実装されている内容も今のPythonならシュッとかけるのかもしれません。
これ以上は、今回のエントリーの本題から外れていきそうなので話を戻します!

PHPにも同様の機能が導入されています。

qiita.com

yieldと非同期処理

ここまで見てきたとおりで、コルーチンは「別の何かをする処理」であると言えます。そして、「中断して(=処理を呼び出し元に戻して)」「再開して(=自身の中に状態を維持・継続ができる)」というのがポイントです。
ということは、「コルーチン内部の処理を行っている最中に、呼び出し元の処理をブロックしない」もしくは「取り急ぎ呼び出し元にコントロールを戻しつつ、自分の処理もちょいちょい進めておく」事が可能なら、実質的に非同期処理が可能という着想で合っていますか。

まさに、そのような内容を扱っている資料が上がっていました。

これは、以下の条件によって実現されているものと思います

  • non-blocking APIを備えた実行内容がある
    • curl_multi
  • coroutineをstackさせておく
  • stackされたリクエストの解決まで、無制限のループ処理によって状態のチェックを行う

そうすることで、「すべての解決を待つ」ことをしつつ「個別の処理は並列的に行う」という操作が可能になったという理解をしています。

「PHPでコルーチンを利用することはできるが、それ自体は「あらゆるものをノンブロッキングにする」ような能力はなく、あくまで並列化の可否は処理内容としてノンブロッキングAPIを利用出来るか次第で、また「整いました!」とイベントを拾いに行くのは自分で監視をじっそうしなければならない・・・」 こんなところでしょうか。

ReactはイベントループとノンブロッキングI/Oの実現を目指した、ループ機構です

感想

「なんでもすぐに並列化できちゃうすごいやつ!」というのは、PHPという言語上は存在しないんだなーというのを改めて認識しました。
その一方で、ノンブロッキングな処理が実装されている領域についてはPHP上で実装が可能そうにも思います。例えばMySQLからのデータ取得などは、個人的な関心からいっても恰好のターゲットです。
実際に手を動かしてみないとイメージ湧かないな〜とも感じたので、「引き続き模索してみたいな」と思っています。

qiita.com

その他の文献

本文中では触れなかったものの参考にした記事です