Python3.4のAsyncioを理解する(1)

この記事は、GoogleのEngineerであるSahand Sada氏のコチラの記事の訳をベースとしています。単純訳ではなく、編集加えてます。許可ももらっています

この記事は、長いため複数回に分けて執筆しています。

複数の処理の並列実行を考える

下記の機能を持つプログラムの実装を考えてみましょう。

  • 3秒毎に”Hello world!”と印字する
  • ユーザからの入力を待つ
    • ユーザは正の数nを入力する
    • 値が入力されるとフィボナッチ数列F(n)を計算し、結果を出力する
    • 結果を出力後は、またユーザの入力の待ちに入る

ユーザが値を入力している間に、”Hello world!”が印字されるということも起きえますが、それについては今回は考慮しません。

Node.jsでの実装

Node.jsを使って実装すると下記のようになります。

Node.jsを使えば、このようなプログラムの作成は簡単です。必要な作業としては、”Hello world!”をプリントする間隔を設定することと、process.stdinのdataイベントにハンドラを設定することです。実際に動かしてみると、意図したとおりに動くことが分かります。より深く理解するために、Pythonで同様のコードを実装してみます。

Pythonでの実装

今回の実装では、フィボナッチ数列の計算時間を表示するために、
log_execution_timeというデコレータを作成し、使っています。

ここでフィボナッチ数列の計算のために使用しているアルゴリズムは敢えて計算速度が最も遅いアルゴリズムを選定しています。(この投稿は、フィボナッチ数列の計算がメインでは無いことと、デモとして実行時間がかかるほうが分かりやすいため)

仕様を満たすようなプログラムをPythonで実装する場合には、どうすればよいでしょうか?PythonにはJavascriptのようなsetIntervalやsetTimeoutのようなネイティブなメソッドは提供されていないので、別の方法を考える必要があります。

まず考えられるのは、OSレベルで提供されている同時実行の仕組みです。というわけで、2つのスレッドを使ってみましょう。

プログラム自体は非常にシンプルですね。このスレッドペースのPythonプログラムと、Node.jsによる実装にはなにか違いがあるのでしょうか?ちょっと実験をしてみましょう。

実験

上述のように、今回使用しているフィボナッチ数列計算アルゴリズムは遅いものですが、より大きい値を引数として与えてみましょう。Pythonには37、Node.jsには45を与えてみます。(数値計算については、Javascriptの方がPythonよりも高速であるため)

ここでは、フィボナッチ数列の計算に9秒ほどかかっていますが、その間にも”Hello world!”はプリントされています。Node.jsの方を実行すると、下記のようになります。

Pythonとは違い、フィボナッチ数列の計算をしている間は、”Helllo world!”のプリントは止まっています。この違いについて、少し考えてみましょう。

スレッドとイベントループの違い

前節での違いを理解するためには、スレッドとイベントループの違いの理解が必要です。

スレッド

一つのスレッドで実行されるシンプルな同期プログラムでは、その命令が完了するまでプログラムの実行が中断されるのはこのためです。もっともシンプルなブロッキング命令はsleepです。(実際、sleepは名前の通り指定された時間、スレッドをブロッキングします)

プロセスは、複数のスレッドを持つことが出来ます。同一プロセスに含まれる複数のスレッドは、メモリやアドレス空間、ファイルディスクリプタなどのプロセス単位に管理されるリソースを共有することが可能です。

OSが別のスレッドに実行を切り替えるきっかけは多くあります。例えば、別の優先度の高いプロセスやスレッドがある場合、スレッド内でスレッド自身を停止した場合(sleep関数とか)、スレッドが自分に割り当てられた時間を使い切った場合(この場合は、スレッドキューの後ろに入れられ次回の実行を待つことになります)などです。

上記のサンプルプログラムについて考えると、Pythonは明らかにマルチスレッドです。CPUパワーを使うフィボナッチ数列計算が、他のタスクの実行を阻害しなかったのはこのためです。

Node.jsについてはどうでしょうか?フィボナッチ数列計算が、他のタスクを停止したことを見ると、シングルスレッドで実行されているということが分かります。そして、これはまさしくNode.jsがどのように実装されているかということにつながるわけです。

このように一見便利に見えるマルチスレッドですが、場合によってはマルチスレッドを採用すべきでない場合があります。たとえば、CPUやメモリのリソースを消費したくない場合や、複数のスレッドの競合状態、デッドロックを管理しなければならない場合です。

イベントループ

マルチスレッドを使わずに同様のことができないでしょうか?そこでNode.jsが採用している処理方法に着目してみます。それはイベントループです。

まず、stdinにユーザのインプットが無いかどうかを定期的にチェックします。もし、inputがあるならばそれを読み込み、処理をします。OSによって、様々なシステムコールがありますが、Python3.4で追加されたselectorsモジュールではこの仕組が抽象化されており、OSを気にすることなく使用することが可能です。

イベントループは非常にシンプルになります。

  1. インプットがあるかどうかをチェックする
    • もしインプットがあるならば読み込み、処理をする
  2. 最後に”Hello world!”とプリントしてから3秒以上経過しているかどうかをチェックする
    • もし経過しているならば、もう一度プリントする
Pythonによるイベントループの実装

実装は下記のようになります。

また出力は下記のようになります。

このプログラムでは、Node.jsと同じように動作することが分かります。具体的には、シングルスレッドで動作しているため、フィボナッチ数列計算が”Hello world!”をプリントするタスクをブロッキングしています。

これである程度整理は出来ましたが、もう少し汎用的な仕組みを検討したいところです。