Django 3の目玉の1つ「ASGI対応」

福田隼也氏:「Django 3.2 ASGI対応 - こわくないasyncio基礎とasync viewの使い所」ということで、今日お話しします。福田です、よろしくお願いします。

まずお前誰だよというところで、福田隼也と申します。「Twitter」は@JunyaFffといいます。長野県の会社の株式会社日本システム技研に所属しています。

Webエンジニアをしていて、弊社の運営する「GEEKLAB.NAGANO」のコミュニティスペースの運営にもかかわっています。また、最近では『Effective Python』の読書会をやっています。私は参加メンバーですが、興味ある方はぜひご参加ください。

ほかにも仕事で「バリューブックス」という本のサービスにも携わっています。みなさん本はお好きですか? 私は大好きです。古本の買取を行ったり、古本市場を著者や出版社に還元する取り組みを行っていたりします。今14周年キャンペーン中なので、ぜひご覧になってみてください。

私は長野が大好きなのですが、ハイブリッド開催、おめでとうございます。私は初めてのオフライン参加です。ここ1年いろいろなものがオンラインだったので、インターネットの向こう側にいる方々にお会いできてとても光栄です。

では、Djangoです。「Django 3」が出てから初めての「DjangoCongress JP」です。Django 3の目玉の1つに、「ASGI対応」があります。3.1では「Asyc View」「ミドルウェア」「テストクライアント」のサポートがあります。

今回は「ASGI」と「Asyc View」の2つと、3.x、または4で来るかなと言われている「Async ORM」について紹介できればなと思っています。

ASGI対応はWebサーバーとWebアプリケーションのasyncio対応

さて、今回この話を聞いてもらううえで、重要なポイントがあります。ASGI対応ってなに? という話です。Webサーバーとフレームワークをつなぐ仕様がASGIです。WSGIの精神的後継と言われています。

「Gunicorn」や「uWSGI」といったWSGI対応のWebサーバーと「Django」「Flask」「Pyramid」などのWSGIのWebフレームワークとつなぐのがWSGIです。

「Uvicorn」や「Hypercorn」といったASGI対応のWebサーバーと「Django」「FastAPI」「Starlette」などのASGI対応のWebフレームワークをつなぐ仕様というところでは、ASGIもWSGIも同じです。

では、何が違うのでしょうか? 名前を見てみましょう。WSGIはWeb Server Gateway Interfaceで、ASGIはAsynchronous Server Gateway Interfaceです。Asynchronous、非同期のServer Gateway Interface。サーバーのインターフェイスです。

WebサーバーとWebアプリケーションのasyncio対応が、ASGI対応ということになります。つまり、DjangoのASGI対応はasyncio対応ということです。

さて、本日のお品書きです。ASGIを使用するうえで必要なasyncioの基礎、そしてDjango Async Viewの使い所。最後に、Django 3.x、か4で来ると言われているAsync ORMについてお話できればと思います。DjangoConなのですが、asyncio成分が多めです。よろしくお願いします。

asyncioを怖いと思っていた

モチベーションとゴールです。私自身、asyncioは怖いなと思っていました。「Django Async Viewって何がおいしいの?」とか「Django ORMもいけるの?」とか思っていました。

このトークで、「asyncioがちょっと怖くなくなったかも」「Async Viewってそういうことね」「async ORM楽しみだな」などと思ってもらえると幸いです。

「すべてはasync/awaitから始まった」これは、ブログからの引用です。かっこいいから使わせてもらっています。「asyncioはこわくない 」asyncioの紹介をします。Python3.4で追加された標準ライブラリで、3.5がasync/await、3.6は非同期ジェネレーターと非同期内包表記。3.7以降もアップデートされています。

並行処理のコードを書くための標準ライブラリです。イベントループという仕組みを利用してシングルスレッドで並行処理を実現します。

RFC(Request for Comments)で標準化されたWeb規格の「QUIC」、HTTP3のあれですね。そのリファレンス的実装に「aioquic」というものがあります。これもasyncioでできています。標準ライブラリであるasyncioです。TCPやUDPのサーバーの実装もasyncioでできてしまうんです。

asyncioはめっちゃ怖いです。「わけわかんない。なんでもできるの最強なの?」と思っていました。怖かったのですが、2つの出会いがあって、少し怖くなくなりました。1つは『Using Asyncio in Python』という株式会社オライリー・ジャパンから出ているカエルの本と、もう1つは『コルーチンは怖くない』という「Minimum Viable Programmer」というブログの記事でした。

この2つの出会いのあと、いろいろな書籍のasyncioの解説や公式ドキュメント、関連記事がスーっと入ってくるようになりました。この場で感謝を述べたいと思います。

asyncioは超巨大です。公式ドキュメントも高レベルAPIと低レベルAPIに分かれています。quicの実装もできてしまうぐらい、いろいろできます。今日DjangoConに来ている私たちは、アプリケーション開発者です。私は、フレームワークやライブラリの開発に憧れているアプリケーション開発者です。フレームワークやライブラリ開発者向けの機能は、ちょっと置いておきましょう。

アプリケーション開発者向けの内容でお伝えしたいと思っています。前提知識として、言葉の説明と、asyncioで私たちアプリケーションエンジニアにとって必要なことをしゃべりたいと思います。

「asyncio」は非同期IOで並行処理を行う

さて、asyncioを分解していきましょう。なによりも知らないということが怖いですね。まずは並行処理。ご存知の方も多いと思いますが、やっぱり難しそうなイメージがあります。それと、イベントループと非同期IOです。

公式ドキュメントの引用です。asyncioは、async/await構文を使って、並行処理のコードを書くためのライブラリです。非同期IOで並行処理をやるみたいです。

並行処理を学んでいくと、似た言葉に出会います。1つは並列処理です。タスクの処理の仕方が違っていて、それぞれ得意な処理が違います。

順次処理は、ひと続きのタスクを完了させてから次のタスクに着手します。並列処理は、複数のタスクを同時に着手します。最後に並行処理です。タスクを少しずつ分割して処理します。

Pythonでいうと、順次処理は通常の関数を呼び出していく処理です。並列処理は、multiprocessingモジュールや、concurrent.futuresのProcesspoolExecutorです。

そして、並行処理。並行処理はthreadingモジュール、concurrent.futuresのThreadpoolExecutor、そしてasyncioです。

ちょっとわかりにくいので、レストランに例えてみます。どんなタスクがレストランにはあるでしょうか?1つ目は注文を取る。2つ目、料理を作る。3つ目は、料理を運ぶ。これをそれぞれの処理に当てはめていきましょう。

順次処理です。このように注文を取って、取り終わってから料理を作って、料理を運びます。完了してから次のタスクをこなしていきます。

次に並列処理。並列処理は、同時に行います。マルチプロセスで言うと、タスクを対応する人が3人いて、すぐに意思の疎通ができるイメージです。複数のタスクに同時に着手するのが並列処理です。

最後に、並行処理です。並行処理は、それぞれのタスクを少しずつ進めます。さらにこれをasyncioの場合に置き換えてみましょう。asyncioはシングルスレッドです。シングルスレッドはレストランで例えるとワンオペレーションです。1人ですべてのタスクをこなしています。よく見ると、同じ顔です。タスクをちょっとずつこなす。さて、どのタイミングでタスクの切り替えをするのでしょうか?

あなたはレストランを1人で回しています。すべてのタスクを1人でこなさなければなりません。お客さまに声をかけられて、タスクの1つである注文の対応をしています。お客さまから食べたいものを聞き出す。声をかけられた時に、考え中のお客さま。1人でこなしているからほかにもタスクはあるのに……余裕がない時は早くしてくれと思うかもしれません。

あなたから見て、他人であるお客さまに、注文という入力の状態を待つという、ここがタスクを切り替えるタイミングです。あなたから見て外部のお客さまから、注文という入力を待つ状態になります。これが非同期IOです。

asyncioの文字を見てみましょう。「入力、ioと同期しない」。これがasyncio、非同期IOです。

asyncioは、ワンオペレーションのレストランで、注文待ちの時に瞬時にタスクを切り替えてこなしてくれます。タスクの外部IOの待ち時間を待たずに、他のタスクを並行に実行する。それが 「asyncio」です。さて怖かったasyncioですが、ちょっとまとめです。並行処理をちょっとずつやります。シングルスレッド、ワンオペレーションです。1人で回します。非同期IO、つまり外部のお客さまからの注文という入力を待たないでほかのことをします。

アプリケーション開発者にとって必要な「イベントループ」「コルーチン」「タスク」

本題のDjangoまでもう少しお付き合いください。言葉は完全にイメージできました。まだasyncio自体の使い方はよくわかりません。アプリケーション開発者にとって必要なものを見ていきます。

1つは「イベントループ」、次に「コルーチン」最後に「タスク」です。すごく雑に言うと、コルーチンは、やること。タスクはコルーチンプラス実行状態を持ちます。コルーチンの上位のものとイメージしてください。最後に、イベントループです。イベントループは、やることを管理するものです。ちょっとわかりにくいですね。またイメージをしてみたいと思います。レストランの例です。

先ほどの並行処理ですが、オペレーションの一つひとつは、コルーチンやタスクです。ではイベントループとは、この例えでいうと何者なのでしょうか?

彼女がイベントループです。タスクやコルーチンの中断、再開を管理するものです。タスクやコルーチンは彼女の手のひらの上です。asyncioでは、このイベントループを作り出して破棄することができます。(彼女はあくまでイメージです。)

どう実装していくかを見ていきたいと思います。まずはコルーチンからです。コルーチンとは、処理の仲間で広い意味での関数の一種です。「Co-routine」と書きます。

処理を途中で中断して、再開できるルーチンです。関数の定義にasyncを付けて、中断して再開するところにawaitを付けます。先ほどの例でいうと、お客様の注文の待ちの状態のところです。これが、asyncioを使う上での「基本のキ」です。

では、定義してみましょう。関数の定義はdef というふうにします。コルーチンの定義は、asyncを付けるだけです。これでコルーチンになります。defで定義した関数とほぼ変わりません。

時間のかかる処理にawaitを付けています。この場合、2行目のasyncio.sleepに付けていますね。そこから中断を再開します。この時点での、中断と再開の意味はちょっと置いておいてください。awaitを付けることができるのは、コルーチンやタスクなどです。

普通の関数は、実行するとこのように返ってきます。コルーチン関数を同様の呼び方をしてみましょう。すると、オブジェクトが返ってきました。これがコルーチンオブジェクトです。コルーチンは関数と同じように実行するだけでは、結果を受け取れません。では、どうやって実行するのでしょう?そこで登場するのがイベントループです。コルーチンのスケジューリングを管理します。

彼女の手のひらの上で転がされてみましょう。いったいどういうものなのか、歴史とともにちょっと解説したいと思います。概念の生まれは、C10K問題という問題に対する回答で、クライアントの同時接続数が多くなるとサービスの応答が遅くなるという問題です。libuv(nodejs)の中核をなす仕組みとして生まれました。

イベントループは、コルーチンやタスクのスケジューリングをします。協調して並行に実行するための仕組みです。私たちアプリケーション開発者にとっては、そこまで意識はしてなくてもいいものです。ただ、そこにいることは知っていてほしいと思います。asyncioの中核をなす仕組みがイベントループです。

python 3.7で追加された「asyncio.run」というものがあります。これはイベントループを作って、タスクが終わったら削除をしてくれます。asyncio.runは、coroutineを受け取ることができます。

イベントループの中には、loopオブジェクトがあって、実はいくつかほかにも知ってほしいものがありますが、時間の関係でまたの機会にお話しします。

では、実際にイベントループを使ってみましょう。「customers_long_thinking_order」という「ちょっと待ってね」という文から始まって、「ざるそば」を返すというコルーチンがあります。注文を受けてみましょう。thinkingコルーチンにawaitを付けて、値を受け取りprint()に戻します。

「asyncio.run(order())」でイベントループを使って実行した結果が、下にあります。「ご注文は?」というのが、order()の最初のprint()です。ちょっと待ってねと言われて、ずいぶん長考したあとに、ざるそばという注文を受け取って、コルーチンに戻します。

このように普通の関数のようにreturnを受け取ることができます。終わったらイベントループは削除しています。

(次回へつづく)