このページを正しく表示するにはJavascriptを有効にしてください。
ジェネレータを使ってコールバック地獄を回避するTornadoの非同期処理がすごい
PythonのWebフレームワークTornadoを使っていて、非同期処理の書き方が素晴らしかったのでシェア。
## 非同期型Webサーバー
アプリ・フロントエンジニアにとっては非同期処理は無くてはならないくらい重要なものですが、バックエンド側ではあまり馴染みがないかもしれません。
普通のWebサーバーはリクエストを受けてから、レスポンスを返すまで1つの処理フローが途切れず続いています。リクエストを解析して、モデルからデータを取ってきて、表示用にフォーマットしなおして…みたいな処理をプログラム通り順次実行していきます。処理の流れがシンプルで分かりやすいのですが、データベースにアクセスしたり、外部への接続などといったことを行うときは、接続先から結果が帰ってくるまで待つしかありません。これが同期処理と呼ばれるもので、向こうの完了を待って(同期して)次の処理を行っていく形となります。
```python
"""
同期的な擬似コード例
"""
def get(request):
params = request.params
# 時間がかかる処理
items = get_items(params)
# get_itemsが終わるまで次の行に行かない
return render("hello.html", items)
def get_items(params):
# DBアクセスなど
items = hogehoge(params)
return items
```
この待ち時間を有効に使うために非同期型のWebサーバーが出てきました。一番有名なのはNode.jsですね。Pythonでは今回のTornadoみたいなフレームワークがあります。この非同期型の一番の特徴は処理の完了を待たないという点です。データベースへのアクセスなど時間の掛かる処理を行うときは、ある処理(A)を呼び出したタイミングで一度リクエスト側の処理を終了させ、処理が完了した段階でAの方からリクエストの続きの処理を呼び出す(いわゆるコールバック)という形式を取っています。待ち時間を有効に使えるのでより多くのトラフィックを捌けるようになるのが特徴です。ただその反面処理の流れがあっちこっちに飛ぶので記述が複雑になりやすい(コールバック地獄)という欠点があります。
```python
"""
非同期的な擬似コード例
"""
def get(request):
params = request.params
# 処理が終わったらcallbackを呼び出す
async_get_items(params, callback)
# 続きの処理
def callback(result):
return render("hello.html", result)
def async_get_items(params, callback):
# 非同期で呼び出される関数
def async_task():
items = get_items(params)
# 処理が終わったらこちらから続きの処理を呼び出す
callback(items)
# async_taskを実行する
# async_execute関数自体は呼び出した瞬間すぐ終わる
async_execute(async_task)
def get_items(params):
# DBアクセスなど
items = hogehoge(params)
return items
```
同期的なコードと比べてぱっと見では流れが把握しづらくなってしまいます。今は非同期処理が1回だけなのでこの程度で済んでいますが、あちこちからデータを取りに行ってマッシュアップするような処理を書こうとするとコールバックだらけになってしまって大変見づらいです。
では本題のTornadoではどのように書けるのか、というと以下のようになります。
```python
# 非同期を扱えるようにするためのデコレータ
@gen.coroutine
def get(self):
params = self.request.params #FIXME:
# 非同期処理を呼ぶ時にyeildを付ける
items = yield get_items(params)
self.render("hello.html", **items)
@gen.coroutine
def get_items(self, params):
# DBアクセスなど
items = hogehoge(params)
# returnではなく Returnメソッドの結果をraiseする
# Python3.3以降であればただのreturnでよい
raise gen.Return(items)
```
これで非同期処理が書けてしまいます。比べてもらえれば分かると思いますが、同期的なコードにかなり近い形で記述することが可能です。コールバック関数を用意する必要もないので処理の流れを追うのも簡単です。
ポイントとしては@gen.coroutineというデコレータを利用する点ですね。Pythonではデコレータという関数を装飾する機能が備わっていて、関数の前に@(アット)を付けてあげるだけで、対象の関数をラッピングする事が出来ます。今回の場合はgetとget_itemsという関数をそれぞれgen.coroutineという関数でラッピングしていることになります。
このgen.coroutineデコレータの役割は対象の関数をジェネレータへ変換し、ジェネレータが終了するまで非同期で繰り返し呼び出す作業を行ってくれる事です。ざっくり言えばyieldしたところで一旦処理を中断して、非同期処理の結果を待ってくれます。このデコレータのお陰で同期処理のようにシンプルなコードで実現する事ができるようになるわけです。
デコレータとジェネレータ組み合わせて非同期処理をシンプルにするこの発想はすごいと思いました。Tornado面白いです。
## リファレンス
* [http://tornado.readthedocs.org/en/latest/gen.html](http://tornado.readthedocs.org/en/latest/gen.html)