はじめに
asyncio ライブラリはリクエストを非同期処理で行うため、リクエスト中にブロッキングが発生しません。一方で requests ライブラリはリクエストを同期処理で行うため、リクエストが完了するまで次の処理に進むことができません。
この記事では以下について書きます。
- aiohttp を使ってみる
- requests と aiohttp の動作の違いを確認する
- 考察
バージョン:
Python: 3.10.12
aiohttp: 3.12.4
requests: 2.32.3
aiohttp を使ってみる
まずライブラリをインストールします。
pip install aiohttp
PyPI:
公式ドキュメント:
インストール後、以下のファイルを作成し、実行します。
import asyncio
import aiohttp
async def req_aiohttp(name):
print(f"{name} のリクエストが開始しました。")
async with aiohttp.ClientSession() as session:
async with session.get("https://httpbin.org") as response:
print("Status:", response.status)
print("Content-type:", response.headers["content-type"])
html = await response.text()
print("Body:", html[:15], "...")
print(f"{name} のリクエストが完了しました。")
async def main():
await req_aiohttp("Alice")
if __name__ == "__main__":
asyncio.run(main())
実行結果:
------ Alice のリクエストが開始しました。------
Status: 200
Content-type: text/html; charset=utf-8
Body: <!DOCTYPE html> ...
------ Alice のリクエストが完了しました。------
requsts と aiohttp の動作の違いを確認する
並列で2人のユーザーがリクエストを実行してみます。3 秒かかるリクエストを実行します。
以下のサイトはリクエスト送信からレスポンス受信まで 3 秒かかります。
aiohttp
検証コード:
import asyncio
from datetime import datetime
import aiohttp
async def req_aiohttp(name):
print(f"\n------ {datetime.now().strftime('%H:%M:%S.%f')} - {name} のリクエストが開始しました。 ------\n")
async with aiohttp.ClientSession() as session:
async with session.get("https://httpbin.org/delay/3") as response:
print(f"{name} Status: {response.status}")
print(f"\n------ {datetime.now().strftime('%H:%M:%S.%f')} - {name} のリクエストが開始しました。 ------\n")
async def main():
users = ["Alice", "Bob"]
tasks = [asyncio.create_task(req_aiohttp(user)) for user in users]
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
実行結果:
------ 00:28:44.004863 - Alice のリクエストが開始しました。 ------
------ 00:28:44.005018 - Bob のリクエストが開始しました。 ------
Bob Status: 200
Alice Status: 200
------ 00:28:48.030639 - Alice のリクエストが完了しました。 ------
------ 00:28:48.031345 - Bob のリクエストが完了しました。 ------
Alice、Bob のリクエストは同時に開始・完了しています。
requests
検証コード:
import asyncio
from datetime import datetime
import requests
async def req_requests(name):
print(f"\n------ {datetime.now().strftime('%H:%M:%S.%f')} - {name} のリクエストが開始しました。 ------\n")
with requests.Session() as session:
with session.get("https://httpbin.org/delay/3") as response:
print(f"{name} Status: {response.status_code}")
print(f"\n------ {datetime.now().strftime('%H:%M:%S.%f')} - {name} のリクエストが完了しました。 ------\n")
async def main():
users = ["Alice", "Bob"]
tasks = [asyncio.create_task(req_requests(user)) for user in users]
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
実行結果:
------ 01:14:16.735965 - Alice のリクエストが開始しました。 ------
Alice Status: 200
------ 01:14:20.595169 - Alice のリクエストが完了しました。 ------
------ 01:14:20.596051 - Bob のリクエストが開始しました。 ------
Bob Status: 200
------ 01:14:24.445815 - Bob のリクエストが完了しました。 ------
Alice のリクエスト完了後、Bob のリクエストが開始しています。
検証結果まとめ
以下のことが確認できました。
同期処理(requests):
[ Alice 開始 ] ----3秒---- [ Alice 終了 ] -> [ Bob 開始 ] ----3秒---- [ Bob 終了 ]
非同期処理(aiohttp):
[ Alice 開始 ] [ Bob 開始 ]
----3秒----
[ Alice 終了 ] [ Bob 終了 ]
考察
requests ではなぜブロッキングが発生するか
requests のコードでは、async with
を使っていません。async with
で実行すると以下のエラーが発生します。requests は同期処理のため使いたくても使えません。
------ 00:43:52.276705 - Alice のリクエストが開始しました。 ------
------ 00:43:52.276879 - Bob のリクエストが開始しました。 ------
Traceback (most recent call last):
File "/home/oizumi/python-learning/asyncio/sample_requests.py", line 27, in <module>
asyncio.run(main())
File "/usr/lib/python3.10/asyncio/runners.py", line 44, in run
return loop.run_until_complete(main)
File "/usr/lib/python3.10/asyncio/base_events.py", line 649, in run_until_complete
return future.result()
File "/home/oizumi/python-learning/asyncio/sample_requests.py", line 24, in main
await asyncio.gather(*tasks)
File "/home/oizumi/python-learning/asyncio/sample_requests.py", line 9, in req_requests
async with requests.Session() as session:
AttributeError: __aenter__
そのため、requests のコードでは以下と同じ状態になっています。
import asyncio
import time
async def wait_function(name):
print(f"------ {name} は 3 秒待機します。 ------")
# ここでブロッキング
time.sleep(3)
print(f"------ {name} の待機が終わりました。 ------")
async def main():
users = ["Alice", "Bob"]
tasks = [asyncio.create_task(wait_function(user)) for user in users]
await asyncio.gather(*tasks)
if __name__ == '__main__':
asyncio.run(main())
実行結果:
------ Alice は 3 秒待機します。 ------
------ Alice の待機が終わりました。 ------
------ Bob は 3 秒待機します。 ------
------ Bob の待機が終わりました。 ------
並列処理を行うためには以下のように await
を使う必要があります。
import asyncio
import time
async def wait_function(name):
print(f"------ {name} は 3 秒待機します。 ------")
# ブロッキングは発生しない
await asyncio.sleep(3)
print(f"------ {name} の待機が終わりました。 ------")
async def main():
users = ["Alice", "Bob"]
tasks = [asyncio.create_task(wait_function(user)) for user in users]
await asyncio.gather(*tasks)
if __name__ == '__main__':
asyncio.run(main())
実行結果:
------ Alice は 3 秒待機します。 ------
------ Bob は 3 秒待機します。 ------
------ Alice の待機が終わりました。 ------
------ Bob の待機が終わりました。 ------
非同期関数内で同期リクエストを並列処理する
イベントループを使用します。以下の記事を参考にしました。
公式ドキュメント:
検証コード:
import asyncio
from datetime import datetime
import requests
def sync_request(name):
print(f"\n------ {datetime.now().strftime('%H:%M:%S.%f')} - {name} のリクエストが開始しました。 ------\n")
with requests.Session() as session:
with session.get("https://httpbin.org/delay/3") as response:
print("Status:", response.status_code)
print(f"\n------ {datetime.now().strftime('%H:%M:%S.%f')} - {name} のリクエストが完了しました。 ------\n")
async def req_requests(name):
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, sync_request, name)
async def main():
users = ["Alice", "Bob"]
tasks = [req_requests(user) for user in users]
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
実行結果:
------ 01:15:44.411194 - Alice のリクエストが開始しました。 ------
------ 01:15:44.411489 - Bob のリクエストが開始しました。 ------
Alice Status: 200
------ 01:15:48.260434 - Alice のリクエストが完了しました。 ------
Bob Status: 200
------ 01:15:48.261894 - Bob のリクエストが完了しました。 ------
疑問点
aiohttp では以下のように完了メッセージが並んで表示されていました。
Bob Status: 200
Alice Status: 200
------ 00:28:48.030639 - Alice のリクエストが完了しました。 ------
------ 00:28:48.031345 - Bob のリクエストが完了しました。 ------
しかし run_in_executor
の場合、以下のように完了メッセージが並んでいません。
Alice Status: 200
------ 01:15:48.260434 - Alice のリクエストが完了しました。 ------
Bob Status: 200
------ 01:15:48.261894 - Bob のリクエストが完了しました。 ------
この理由がわかっていません。
まとめ
- aiohttp はリクエストを非同期処理で行うため、リクエスト中にブロッキングが発生しない
- requests はリクエストを同期処理で行うため、並列処理を行うにはイベントループが必要
参考