はじめに
FastAPIを始めてみたところ、async/await構文があり、Pythonにも「async/await構文があるんだなー」と初めてその存在を知った。
しかし、FastAPIのサンプルコードやネットで公開されているコードを見ると、async def
とdef
をどのように使い分けているのかよくわからず、結局、「どっちを使えば良いんじゃろう?」という気持ちになったので、async/await、同期 / 非同期(並行処理)を調べつつ、結論を導いてみることにした。
いきなり結論
Path operation 関数
の場合、async def
ではなくdef
で、基本、実装する。
-
def
だけでも、外部スレッドプールで非同期処理されるようにフレームワークとして実装されているとのこと -
async def
を使った方が良いのは以下の2ケース- async/awaitをサポートしているライブラリを利用したい場合
- 「I/Oバウンド」が発生しない場合
以下、コードの@app.get('/')
から始まる関数をPath operation 関数
と呼ぶ
@app.get('/')
def results():
results = some_library()
return results
そもそも同期 / 非同期(並行処理)とは何か
まずは、そもそも同期 / 非同期(並行処理)を復習しないと、async/await構文をちゃんと理解できないと思うので、同期 / 非同期を復習する。
同期処理とは
複数のタスクを実行する際に一つずつ順番にタスクを実行する処理のこと
「タスク1」「タスク2」を同期処理するアプリケーションがあったとする。このアプリケーションが、ユーザーAからリクエストを受けた場合を例にとって説明する。
プログラムに書かれた通りの順番でタスクが処理されるので、タスク2が終わるまでタスク1の処理が中断され、ユーザーからは画面が固まったように見えてしまう。
*出典:[非同期処理とは? 同期処理との違い、実装方法について解説]
(https://www.rworks.jp/system/system-column/sys-entry/21730/)
非同期処理(並行処理)とは
あるタスクを実行している最中に、実行中のタスクを止めることなく別の新しいタスクが実行できる処理のこと
*Pythonのasync/awaitは、並行処理でシングルスレッドで動作し、マルチスレッドではない点に注意。マルチスレッドは並列処理にあたる。
「タスク1」「タスク2」を非同期処理するアプリケーションに、ユーザーAから「タスク1,2」を処理するリクエスト、ユーザーBから「タスク1」のみを処理するリクエストがあった場合で説明する
非同期処理は、あるタスクを実行している最中にその処理を止めることなく別の処理を実行するため、上図のように、ユーザーAのリクエストを処理中にユーザーBからのリクエストがあっても、ユーザーBはユーザーAの処理完了を待たずに、結果を受け取ることができる。
*出典:[非同期処理とは? 同期処理との違い、実装方法について解説]
(https://www.rworks.jp/system/system-column/sys-entry/21730/)
非同期処理(並行処理)はなぜ必要なのか
I/O 操作の待ち時間に並行でタスクを実行することで、全体としての処理時間を削減し、クライアントへのレスポンスを改善するため
*特にFastAPIのようなWebアプリケーションフレームワークにおいては、「I/O バウンド」が多くなるため、非同期処理がパフォーマンス上、重要になってくる
- 実行時間のほとんどがCPUではなく、データ入出力(Input/Outupt)の待ち時間が占めるような処理を**「I/Oバウンド」**と呼ぶ。
- 代表的なI/Oバウンドの例
- ディスクの操作起因
- データをファイルに保存したり、ファイルから読み込んだりする処理
- データベースでCRUD処理した場合
- ネットワーク起因
- ネットワーク経由で、外部のWeb APIに対してリクエストし、レスポンスを受け取る処理
- など。
- ディスクの操作起因
- 代表的なI/Oバウンドの例
async / awaitとは
非同期処理(並行処理)をサポートする構文
*ただし、並行処理のサポートであって、並列処理のサポートではないことに注意
*asyncとは、asynchronous
の略語で非同期という意味。
- async
- 非同期対応のメソッドに定義
- await
- 非同期対応のメソッドを呼び出すときに宣言する
公式マニュアルだとこちらを参照
https://docs.python.org/ja/3/library/asyncio-task.html
async/await 構文を使うためには、並行処理用のasyncioというライブラリを使う。
実装例
以下、同期処理の場合のコードと非同期処理の場合のコードを示す。
同期処理の場合
タスク1が4秒、タスク2が2秒で、計6秒の実行時間。
import time
def say_after(delay, what):
print(f"started say_after {delay} {what}")
time.sleep(delay)
print(what)
def main():
print(f"started at {time.strftime('%X')}")
say_after(2, 'task1')
say_after(4, 'task2')
print(f"finished at {time.strftime('%X')}")
main()
started at 17:44:38
started say_after 2 task1
task1
started say_after 4 task2
task2
finished at 17:44:44
非同期処理の場合(async/await)
タスク1が4秒、タスク2が2秒で、並行で処理されるため計4秒の実行時間。
import asyncio
import time
async def say_after(delay, what):
print(f"started say_after {delay} {what}")
await asyncio.sleep(delay)
print(what)
async def main():
task1 = asyncio.create_task(
say_after(2, 'task1'))
task2 = asyncio.create_task(
say_after(4, 'task2'))
print(f"started at {time.strftime('%X')}")
await task1
await task2
print(f"finished at {time.strftime('%X')}")
asyncio.run(main())
合計時間が4秒であることに注目!
同期処理の場合だと6秒となる。
started at 17:09:57
started say_after 2 task1
started say_after 4 task2
task1
task2
finished at 17:10:01
結局、FastAPIで、async def
は、使った方が良いのか?
結論
-
Path operation 関数
の場合、asyncをつけない、通常のdef
で実装する。- 「I/O バウンドが発生しない場合」、もしくは、「async/awaitをサポートしているライブラリを利用する場合」は、
async def
を使う必要がある。 - ただ、async/awaitをサポートしているライブラリはほとんどないので、現状、"async def"を使う必要は基本的にはない。
- 「I/O バウンドが発生しない場合」、もしくは、「async/awaitをサポートしているライブラリを利用する場合」は、
@app.get('/')
async def read_results():
results = await some_library()
return results
根拠
を読む限りだと、
データベース、API、ファイルシステムなどと通信し、await の使用をサポートしていないサードパーティライブラリ (現在のほとんどのデータベースライブラリに当てはまります) を使用している場合、次の様に、単に def を使用して通常通り path operation 関数 を宣言してください
と書かれており、asyncをつけない、通常のdef
を推奨してます。
また、
を読んでも
async def の代わりに通常の def で宣言すると、(サーバーをブロックするので) 直接呼び出す代わりに外部スレッドプール (awaitされる) で実行されます。
と書かれており、FastAPIのフレームワークとしてawait
するので、実装者側でasync def
を使わなく良いとのことが書かれています。
では、どういう場合に使うのかというと、
path operation 関数 がブロッキングI/Oを実行しないのであれば、async def の使用をお勧めします。
ということで、待ち時間が発生しないようなメソッドはasync def
をつけた方が良いとのこと。
まとめ
-
FastAPIでは、基本的に
async def
を使う必要がない-
def
だけでも非同期処理されるようにフレームワークとして実装している
-
- Pythonのasync/awaitは、 非同期処理(並行処理) を実現する構文である。
- asyncioというライブラリで、async/awaitを使うことで、 「I/Oバウンド」時間を削減し、クライアントのレスポンスを大きく改善することができる。
あとがき
今後、async/awaitをサポートしているライブラリが増えてきた時に、それに即座に対応できるFastAPIって将来を見越してますね!
FastAPIの今後に期待です。
参考文献
- [非同期処理とは? 同期処理との違い、実装方法について解説]
(https://www.rworks.jp/system/system-column/sys-entry/21730/)- 同期/非同期の図解がシンプルでわかりやすい記事
-
python3 の async/awaitを理解する
- 今回のasync/awaitの理解にかなり役立ちました!
-
コルーチンと Task
- 公式のDocument
[宣伝]Udemyの自作教材
私は、2021年7月にwywy合同会社という会社を起業しました。
Qiita記事をキッカケに知名度を少しでも上げたいので、以下自作のUdemyを宣伝させていただければです。
■ Udemy
LINE, AIのAPI, Pythonを使って「AIチャットボットサービス」を公開できるエンジニアになろう!
Python基礎習得後の次の一歩を探している方におすすめ。Googleの感情分析AI、PythonのFastAPI、LINE Botを組み合わせたAIチャットボットサービスの開発を実践的に学びます。