歩いたら休め

If the implementation is easy to explain, it may be a good idea.

【Python】asyncioの代替ライブラリtrioを調べてみた

この記事はPython その2 Advent Calendar 2018の遅れてきた15日目の記事です。

最近、クローリング用のプログラムのasyncioを使った並行処理のプログラミングをしており、「asyncioのベストプラクティス」という趣旨の記事を書こうと思っていたのですが、自分自身感覚を掴めておらず、「あれ?これってasyncioのAPI自体も微妙なのでは?」「そもそもasyncioのドキュメントのドキュメントを読んでも、どの機能をどう組み合わせて使えばいいか分からない」と思ってしまっていて記事を書きかねていました。

ただ、知人から「trioというライブラリのAPI設計が素晴らしい」と薦めて頂いたのと、あまりにも待たせてしまうのも申し訳ないのでひとまず「調べてみた」レベルでアウトプットを出します。

私はまだ並列/並行/非同期処理に詳しいわけではないので、大きな思い違いをしている可能性はあります。鵜呑みにせず、一緒に勉強しましょう。もし変な箇所があればコメント等で連絡ください。

trioとは

「async I/O for humans and snake people」を名乗るライブラリです。requestsなどのAPI設計が優れたライブラリ作者のKenneth Reitzさんっぽい文言ですが、その後に実際に彼のコメントが引用されています。

github.com

まず、こちらのstack overflowの「asyncioとtrioって何が違うの?curioってライブラリもあるそうなんだけど…」という質問に、trioの"primary author"でcurioの"contributor"でもあるNathaniel J. Smithさんが以下のように答えています。

  • asyncioのほうが成熟したライブラリだ
  • trioはコードをもっとシンプルにしてくれる
  • 詳細は多くの違いがある
    • asyncioや関連ライブラリではsome_functionを実行したときにコルーチンが実は完了していない場合もあり、例外やタイムアウト関連で厄介な問題が起きるが、trioの"nurseries"を使う方法ではその動作が完了していることを保証できる
    • trioのタイムアウトや中止の方法が今までにない新しいものである
    • asyncioはAPI設計に冗長な部分や、そのための落とし穴が多い

それでは、「asyncioよりシンプルなAPI」を実現するために、trioではどういう設計になっているのでしょうか?「Async concurrency for mere mortals - PyCon 2018」を引用しつつ紹介します。

Pythonでasync/awaitを使ったコードを書く時、Pythonの同期的な関数の中に、asyncio等のライブラリでasync/awaitの関数を実行して…とプログラミングしていきます。trioのアーキテクチャでは更に"Nurseries"と"Cancel Scope"が追加されています。

asyncioには存在しない"Nurseries"の層では、async with trio.open_nursery()ブロック中でstart_soonでコルーチンをセットしてバックエンドで実行開始し、async withブロックが閉じるときにこれらのコルーチンが処理を完了させている設計になっています。

そうすると、"nursery"のブロックの中でエラーが出たときに、ブロックに例外が通知され適切に処理できることが保証できます。その途中でgoto文論争や構造化プログラミングの話も出ていますが、おそらく「どこで何の処理が行われているか分からないと、適切にエラー処理できない」という例として触れているだけのようです。

次に"Cancel Scope"の説明です。ネットワークを介した処理を行う時、次の3つのことが起こりえます。

  • 成功
  • 失敗
  • どちらでもなく、永久にハングアップする

ハングアップした場合に備えて「必ずタイムアウト処理を書くべき」だとNathaniel J. Smithさんは主張しています。trioでは、async関数の中で「ブロック内の処理がn秒以内に終わらなかった場合はキャンセルする」ような機構が提供されているようです。

Design and internalsによると、この辺りが基となったライブラリの一つのcurioとの大きな違いのようなのですが、両方のAPIに精通しないと読めないですね…。この辺りのAPI設計があまり理解できていないので、tutorialを見てみようと思ったのですが、運悪く「When things go wrong: timeouts, cancellation and exceptions in concurrent tasks」はまだ未整備でした。

関連記事

参考にした記事や、「ここから学んでいけば良さそう」という記事をメモしておきます。

Trio talks

こちらが今回の記事のほぼ元ネタです。この記事では説明し尽くせていない部分もあるので、見ておくといいと思います。

github.com

www.youtube.com

What is core difference between asyncio and trio

stackoverflow.com

Some thoughts on asynchronous api design in a post async/await world

curioとasyncioの異なるアプローチを比較した記事。先述したNathaniel J. Smithさんが書いています。

Some thoughts on asynchronous API design in a post-async/await world — njs blog

Reading list

trioのReading list。trioのアイデアの元になった言語やライブラリが紹介されています。これは骨が折れそうですが、並列/並行処理全般の様々なアプローチを学べそうなので、徐々に読んでいこうと思っています。

github.com

Pythonをとりまく並行/非同期の話

並列・並行処理関連は情報が多くて最初は混乱すると思います。慣れてない方は「CPUプログラムのボトルネックとなる処理がCPUが原因なのか、IOが原因なのか」は重要なのでよく見ておきましょう。

tell-k.github.io

async/awaitはIOが原因となる処理を高速化するための手法です。その後、Nathaniel J. Smithさんの動画を見ると分かりやすいと思います。

あまり関係ないですが、"GIL"のことを"ギル"って発音していたのでビックリしました。自分は"ジル"って読んでたので…。

trioを知る前は「Go, Erlang, Haskellとかの並列・並行処理が得意な(と言われている)言語のアプローチを勉強しようか」って思っていたんですが、もう少し楽できそうです。

最後に

公式リポジトリにもあるように、まだtrioは新しくてやや実験的なプロジェクト(young and still somewhat experimental)のようです。私はプライベートでの開発で試していこうと思います。

他にも、いくつかアドバイスや注意点が書かれています。例えば以下のようなものです。

早くtrioくんと平和な非同期処理の世界に暮らせる日を待ち望んでいます。

わーすた / うるとらみらくるくるふぁいなるアルティメットチョコびーむ MUSIC VIDEO Short Ver. - YouTube

関連ライブラリ・リポジトリ

  • asyncio: みんなご存じ標準ライブラリ。
  • trio: 今回紹介したライブラリです。
  • curio: trioが影響を受けたライブラリの一つ。公式リポジトリの紹介によると、あるベンチマークではtrioの50%程度早いとか。
  • uvloop: asyncioのloopを"ultra fast"なものに置き換えてくれるライブラリ。今回は「使いやすいものを探す」って趣旨ですが一応触れておきます。