複数のAPIを順番に呼び出すと、待ち時間がそのまま積み上がって遅くなります。「asyncioを使えば速くなる」とはよく聞きますが、実際どれくらい速くなるのか、そしてなぜ速くなるのかを、実際にコードを動かして確認します。
この記事は Pythonの非同期プログラミングを攻略!asyncioライブラリの基本と実践活用ガイド の続編です。既存記事が文法中心だったのに対し、本記事は「実際どれだけ速くなるのか」の実測に絞っています。
1. なぜ「待ち時間」だけが短くなるのか
APIを呼ぶ処理の時間は、ほとんどが応答を待っている時間です。CPUは何もしていません。逐次処理では「1件目の応答を待つ→終わったら2件目を送る」と順番に進むため、待ち時間が単純に積み上がります。
非同期処理では、1件目の応答を待っている間に2件目・3件目を送ってしまいます。待ち時間が重なるので、全体の所要時間は「一番遅い1件」に近づきます。
逆に言うと、待ち時間のない処理(純粋な計算)は asyncio では速くなりません。 ここを取り違えると「非同期にしたのに速くならない」ということが起きます。
2. 検証用のコード(外部APIを叩かずに試せます)
公開APIに何十回もリクエストを送るのは相手先に負担をかけます。ここではスクリプト内に小さなHTTPサーバーを立てて、応答が遅いAPIを模擬します。これなら何度実行しても外部に影響しません。
import asyncio, time, threading, http.server, socketserver
import httpx
PORT, DELAY, N = 8123, 0.5, 8 # ポート / 1件あたりの応答待ち秒 / リクエスト数
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
time.sleep(DELAY) # 遅いAPIを模擬
body = b'{"ok":true}'
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args): # アクセスログを黙らせる
pass
class Server(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
srv = Server(("127.0.0.1", PORT), Handler)
threading.Thread(target=srv.serve_forever, daemon=True).start()
URL = f"http://127.0.0.1:{PORT}/"3. 逐次処理と並行処理を並べて書く
def fetch_sequential():
with httpx.Client() as c:
return [c.get(URL).status_code for _ in range(N)]
async def fetch_concurrent():
async with httpx.AsyncClient() as c:
responses = await asyncio.gather(*(c.get(URL) for _ in range(N)))
return [r.status_code for r in responses]並行側のポイントは asyncio.gather() です。複数のコルーチンをまとめて渡すと、すべてを同時に走らせて全部の完了を待ちます。
4. 計測して比べる
t0 = time.perf_counter(); fetch_sequential()
seq = time.perf_counter() - t0
t0 = time.perf_counter(); asyncio.run(fetch_concurrent())
con = time.perf_counter() - t0
print(f"逐次処理 : {seq:.2f} 秒")
print(f"並行処理 : {con:.2f} 秒")
print(f"短縮率 : {(1 - con / seq) * 100:.0f}%")
srv.shutdown()手元の環境(リクエスト数8件、1件あたり応答待ち0.5秒)で3回実行した結果です。
逐次処理 : 4.30 秒 並行処理 : 0.57 秒 短縮率 : 87%
逐次処理 : 4.09 秒 並行処理 : 0.55 秒 短縮率 : 87%
逐次処理 : 4.09 秒 並行処理 : 0.54 秒 短縮率 : 87%3回とも短縮率は安定して87%でした。理論上の下限は「1件あたりの応答待ち0.5秒」に近い値で、実測の並行処理(約0.55秒)はこれにほぼ一致しています。逐次処理の理論値(8件×0.5秒=4.0秒)とも近い数値が出ました。
5. つまずきやすい点
requests を asyncio.gather に渡しても速くなりません。 requests は応答を待つ間ずっと処理を止めてしまうため、非同期の意味がなくなります。非同期で動くHTTPクライアント(httpx.AsyncClient や aiohttp)を使う必要があります。
同期関数の中で await は書けません。 await を使う関数には async def が必要で、入口では asyncio.run() を呼びます。
同時に投げる数は制限したほうが安全です。 相手のAPIに一度に大量のリクエストを送ると、レート制限に触れたり負荷をかけたりします。件数が多い場合は asyncio.Semaphore で同時実行数を絞ります。
参照: asyncio — Python ドキュメント / httpx Async Support
6. まとめ
所要時間(8リクエスト、応答待ち0.5秒/件) | |
|---|---|
逐次処理 | 約4.1〜4.3秒 |
並行処理 | 約0.54〜0.57秒 |
短縮率 | 87% |
速くなる理由は「応答を待っている時間」が重なるからで、待ち時間のない計算処理には効きません。API呼び出しやDBアクセスのようにI/O待ちが支配的な処理であれば、今回のような大きな短縮が見込めます。まずはasyncio.gatherで複数のリクエストをまとめるところから試してみてください。