よくわからずに並行リクエストを書いてた時の話
並行リクエストなのに遅いコード
Pythonの非同期処理(async/await)を使うと、複数の処理を効率よく並行して実行できます。特にネットワークリクエストのような「待ち時間」が発生する処理では効果的です。
非同期処理を触り始めた頃、あるAPIからデータを取得するために、こんなコードを書いたことがあります。
import asyncio
import httpx
async def fetch_data(url: str) -> str:
"""指定されたURLから非同期にデータを取得する"""
with httpx.Client() as client:
response = client.get(url)
return response.text
async def main():
urls = [
'https://example.com/1',
'https://example.com/2',
'https://example.com/3',
]
# 3つのURLから並行にデータを取得
tasks = [fetch_data(url) for url in urls]
# asyncio.gatherは複数のタスクを同時に実行し、すべての結果を待ち受ける関数です
results = await asyncio.gather(*tasks)
# 結果を表示
for result in results:
print(result)
if __name__ == '__main__':
asyncio.run(main())
高速化を図るために、並行にリクエストを送信しようとしていますね。
ただこのコードだと実際には、1つずつ順番にリクエストを送信することになります。
つまり全然高速ではありません。
なぜそうなるのでしょうか?
原因
原因は、client.get(url)
の部分です。
async def fetch_data(url: str) -> str:
"""指定されたURLから非同期にデータを取得する"""
with httpx.Client() as client:
response = client.get(url) # ← ここが問題
return response.text
このclient.get
は同期処理であり、非同期処理ではありません。
同期処理と非同期処理の違いを簡単に説明すると:
- 同期処理:1つの処理が終わるまで次の処理を始めません。料理で例えると、お湯が沸くまでじっとして、沸いてから次の作業をするようなものです。
- 非同期処理:処理が終わるのを待たずに、他の作業ができます。お湯が沸くのを待っている間に野菜を切るようなイメージです。
このコードでは、client.get
は同期処理なので、プログラムはこの行(処理)が終わるまで何もせず止まってしまいます。3つのURLに対して順番に処理することになるため、並行処理の恩恵を受けられません。
解決策は、この行を非同期処理、つまりawait
をつけて呼び出せるようにすることです。
そうすれば、プログラムはこの行の処理を待っている間に、他の処理(他のURLへのリクエスト)を行うことができるようになります。
解決策
意図した通りに動かすには、httpx.AsyncClient
を使用する必要があります。httpx.Client
とは異なり、非同期処理をサポートしています。
async def fetch_data(url: str) -> str:
async with httpx.AsyncClient() as client:
response = await client.get(url) # awaitできる!
return response.text
これでclient.get(url)
が非同期処理として実行されるようになります。
await
がついているので、プログラムはこの行の処理を待っている間に、他の処理(他のURLへのリクエスト)を行うことができます。
この修正により、3つのリクエストはほぼ同時に送信され、最も時間のかかるリクエストが完了するまでの時間だけで全ての処理が終わります。同期処理だと3つのリクエスト時間の合計になるので、大きな差が出ます。
最後に
非同期処理を行う際には、await
をつけるべき関数やメソッドをしっかりと確認することが重要です。
async
というキーワードをつけたからといって、その処理が非同期になるわけではありません。
自戒を込めて、公式のドキュメントなどを参考にしながら、非同期処理を行う関数やメソッドを確認していくことが大切です。
紹介
個人のブログもやっております。自分の経歴なども載せていますので、よければご覧ください。
Discussion