コルーチンをawaitするだけでは並行処理はできない

福田隼也氏:ここまで並行には処理をしていないので、上のループの中で、もう少しawaitの動きを見ていきたいと思います。

考え中のお客さま3人から注文を受けてみましょう。先ほどのコルーチンです。上のところで注文を受けて、下のところでオーダーを受け付けるというところ。それぞれのコルーチンにawaitを付けて、menuを受け取ってみたいと思います。3人は考える時間が異なるので、引数を受け取るように変えて実行しています。

並行実行できていれば、3秒で終わります。結果、6秒かかっています。順番に実行されているようです。さらに動きを確認するために、orderのmenuの出力をそれぞれ受け取った直後にprintしてみましょう。

すると、逐次実行されていることがわかります。1つ目の注文を聞いて、聞き終わってから2つ目の注文を受けているのがわかると思います。コルーチンをawaitするだけでは、並行にはなりません。

ここで登場するのがタスクです。「Taskオブジェクト」というものがあります。コルーチンをラップして実行状態をもちます。イベントループにコルーチンを登録します。

作る方法は大きく2つです。Python3.7で追加された「asyncio.create_task」。あとは並列実行を助ける「gather」があるので覚えておいてください。

「初めてのタスク」です。orderの中で、create_taskでコルーチンのthinking_orderを渡してタスクを受け取ります。自分にawait taskをして、menuをもつコルーチンを受け取る流れになっています。

では、実行してみましょう。処理は3秒で終わっています。先ほどのように、awaitの間にprintを入れてみましょう。コルーチンがいつのタイミングで実行されているのかを確認したいと思います。

menu = await task、という真ん中の下にprintを入れていて、この下も、awaitした後にループで、awaitした後にループで、となっています。どうでしょう。最初のタスク。ここですね。ここが実行された時点で、登録されているすべてのタスクが実行されていることがわかるかと思います。

awaitされた時に中断、再開するのは、すでにイベントループに登録済みのタスクであることがわかるかと思います。

また、結果をまとめて受け取るgatherというものがあります。これは引数にコルーチン、またはタスクを受け取って、すべてが終わり次第、結果を投入した順で返します。

「tasks」というところに登録してあるコルーチンをまとめて実行して、結果を渡した順で返って来るというものになっています。

考え中のお客さまは、3人とも検索しないと決められないんです。なので検索をしてみましょう。なぜ検索をするかというと、asyncioは外部からの入力が欲しいからです。まずは、httpクライアントのrequestsを利用してみましょう。

「long_think」で、responseで受け取る値に「Google」で「おいしいお蕎麦は?」というのを投げています。これを実行してみましょう。

3秒かかりました。どうなんでしょう。この結果だとそれっぽく動いています。この中には実はブロッキングな処理が紛れ込んでいます。asyncio.runにdebugというものがあるんです。もう一度実行してみましょう。すると、時間のかかっている処理を検出してくれます。

メッセージは省略していますが「requests」を利用しているところで、0.915となっています。long_thinkで、requestsを行っているルーチンに何かあるよということがわかります。

asyncioはイベントループを利用して非同期IOを実現するため、IOの部分のモジュールもasyncioに対応している必要があります。requestsはasyncio対応していないため、イベントループである彼女の手のひらの上に乗っかっていない状態です。そのため、asyncio対応のhttpクライアントで実行を試したいと思います。

ここでは、asyncio対応のクライアントの「httpx」を利用してみましょう。実行してみると、処理の終了までに1秒かからずに完了していることがわかるかと思います。

このように、asyncioを利用するモジュールには、モジュール自体がasyncioに対応している必要があることがわかりました。

さて、「asyncioは怖くない」。コルーチンは関数にasyncを付けるだけで、肝はawaitです。タスク、切り替わる先のコルーチン。イベントループに登録しておくと、よきに計らって切り替えてくれます。イベントループは、登録したタスクをawaitのタイミングで切り替えます。ただし、切り替える先のものなので、asyncio対応している必要があります。

それぞれで必要な機能です。コルーチンにはasync/await。タスクにはcreate_taskで、コルーチンを受け取れます。イベントループに登録をします。そしてgatherです。引数にコルーチンをまとめて指定して、まとめて実行結果を受け取れます。イベントループでは、asyncio.runを紹介しました。get_running_loopやget_event_loopがありますが、余力があればまずはこれを抑えましょう。

ここまでお話ししていた中で、いくつかの武器を手に入れました。まずは並行処理。並行処理は処理を少しずつやります。そして非同期IO。外部からの入力を同期しないという概念でした。そしてこれらを実現しているのがasyncioです。コルーチンのタスクとして、イベントループに登録をして処理を並行に実行します。

タイミングとしてキーになるのがawaitです。これは外部からの入力、非同期IO、例ではhttp検索のタイミングをタスクとイベントループによって切り替えます。ちょっとasyncioはわがままで、イベントループで管理する外部と連携している部分は、asyncioに対応している必要があることがわかりました。

ASGI Djangoを実行する方法

ここまでがasyncioです。お待たせしましたDjangoです。やっとここまできました。本日のお品書きはこんな感じです。

タイトルはこんな感じです。Django Async Viewの使い所。ASGI対応とasync view、asyncioのよきところを享受できるようになります。IOの発生する処理で、効果を発揮します。

例えばAPIのリクエストやIoTデバイスとのMQTT。Djangoでそれらを利用したい場合になります。

先ほどのお蕎麦の注文例ですね。この中で、「asyncio.run()はしないの?」と思うかもしれませんが、実は知らず知らずのうちにしています。

ASGI Djangoはどうやって実行しますか? UvicornやHypercornなどのASGI製のWebサーバーを利用しているかと思います。ASGIでPythonのWebサーバーとPythonのWebフレームなどをつなぐのに、ASGIサーバー側でイベントループの実行しています。

非同期な処理を行いたい場合には、ASGIアプリケーションとして実行することがあります。ただ、非同期な処理の中にも、同期の処理、asyncio対応していないものがあるかもしれません。

その場合、同期処理にはDjangoが用意している同期処理を非同期処理にする「sync_to_async」を利用しましょう。

sync_to_asyncは、同期関数を受け取って、それをラップして非同期関数を返します。直接、ラッパーまたはデコレータとして利用できます。

asyncio対応していない同期処理をイベントループに登録できます。Django側は、sync_to_asyncを用意していますが、これはDjangoに限った話ではありません。ほかにも「concurrent.futures」という、threadingとmultiprocessingを容易に使える標準ライブラリがあるので、これらを利用してイベントループに登録できます。

ThreadPoolExecutorとtProcessPoolExecutorを利用して、同期処理をブロッキングすることなくイベントループを利用できるようになります。非同期にはできますが、実行にはコストがかかり、速度に影響がある場合もあります。

ASGI対応とasync viewです。これによって、Djangoでもasyncioと非同期IO/並行処理の恩恵にあずかることができるようになりました。また、同期処理についても、concurrent.futuresを使ったラップが可能です。ただ、まだ残っている同期処理があるんです。最後の砦のDjangoのORMが残っています。

Django ORMはasyncio非対応

Node.jsの作者であるRyan Dahl氏は「データベースにクエリを実行している間、ソフトウェアは何をしているのですか?」と尋ねました。もちろん答えは何もありませんでした。データベースが応答するのを待っていました。

Djangoを利用する上で、大きな魅力の1つであるORMはasyncio対応していません。非同期IOの利点を外部の入出力として説明してきました。主に今回の話の中では、ネットワークでした。

もちろんデータベースへのアクセスも有効です。そこができると全体が速くなります。今触っているアプリケーションを思い出してください。Webでデータベースが非同期になったら、早くなることが想像ができるはずです。

DjangoコアデベロッパーのAndrew Godwinさんは、去年の「PyConline AU 2020」で 「Taking Django's ORM Async」というお話をしていました。

その中で、「APIのデザインが重要だ。ただasync DBAPIというPEPはまだない、課題はある」とお話をしていました。その話の中で、安全な非同期プログラミング、すべてが非同期でエラーを起こせない世界を実現する。非同期を可能な限り最高なものにするとおっしゃっていたのが印象的でした。

具体的なバージョンについてです。Djangoのフォーラムの中での最近のやりとりです。具体的なバージョンは、もう少しかかりそうです。「my initial goal was 4.0, but...」というふうに続きます。

asyncioが多少怖くなくなったと思っていただけるとうれしいです。Djangoのasync view、同期と非同期を併せて、IOバウンドな処理を中心に利用してください。Async ORM、楽しみですね。Pythonでは、ほかにもasyncio対応のORMやアダプターがすでにあります。そちらと併せて利用するのも良いかもしれません。

また、DjangoはOSSです。リポジトリをのぞいてみるのもいいかもしれません。以上です。ご清聴ありがとうございました。