PythonAsyncio徹底解説:2026年のコルーチン、タスク、イベントループ

Table of Contents
Pythonにおける非同期プログラミングは、Python 3.4での実験的なアドオンから、現代の高性能マイクロサービス、APIゲートウェイ、ストリーミングデータパイプラインにおけるデフォルトのアーキテクチャパラダイムへと成熟しました。FastAPI、Litestar、Sanicのようなフレームワークに支えられ、Pythonのバックエンドは数万の同時接続を日常的に処理しています。
しかし、広く採用されているにもかかわらず、asyncioはPythonで最も誤解されているシステムの一つです。開発者は頻繁にイベントループのスタベーションを引き起こしたり、非同期実行コンテキスト内で同期ブロッキング呼び出しを混在させたり、適切な例外分離なしにasyncio.gatherのような古いレガシーAPIに依存したりしています。
この詳細な解説では、Pythonのasyncioランタイムが内部でどのように機能しているのかを解き明かし、asyncio.TaskGroupとレガシーなgatherを比較し、asyncio.to_threadを使ってCPUバウンドな作業を安全にオフロードする方法を実演し、高並行レート制限付きワーカーパイプラインを構築します。
高性能Pythonバックエンドシリーズ
イベントループアーキテクチャ: Pythonが1つのスレッドで10,000個のソケットを処理する方法
asyncioの中心には、シングルスレッドのイベントループがあります。接続ごとにOSスレッドを割り当てる(各スレッドで約8MBのスタックメモリを消費し、OSのコンテキストスイッチングによる大きなペナルティが発生する)代わりに、asyncioはOSのポーリングプリミティブ(Linuxではepoll、macOSではkqueue、WindowsではIOCP)を使用して、ノンブロッキングI/O操作を単一のスレッドに多重化します。
+---------------------------------------------------------------------------------+
| Single OS Thread |
| |
| +-------------------------------------------------------------------------+ |
| | Asyncio Event Loop | |
| | | |
| | 1. Poll OS Kernel Socket Ready State (epoll / kqueue) | |
| | 2. Resume Waiting Coroutines (send next() value) | |
| | 3. Suspend at 'await' boundary (yield socket descriptor to loop) | |
| | 4. Run Scheduled Timers and Callbacks | |
| +-------------------------------------------------------------------------+ |
| | |
| v |
| +-------------------------------------------------------------------------+ |
| | OS Kernel Non-Blocking I/O | |
| | [ Socket 1: Reading ] [ Socket 2: Writing ] [ Socket 3: Connected ] | |
| +-------------------------------------------------------------------------+ |
+---------------------------------------------------------------------------------+
関数がasync defで宣言されている場合、それを呼び出してもすぐに本体が実行されるわけではありません。代わりに、コルーチンオブジェクトを返します。コルーチンは、任意のawait式で実行を中断し、待機中のI/O操作が準備できるまでイベントループに制御を戻すことができる、強化されたジェネレーターです。
現代の構造化並行処理: asyncio.TaskGroup vs asyncio.gather
Python 3.11以前では、複数の非同期操作を並行して実行するための標準的なイディオムはasyncio.gatherでした。広く普及している一方で、gatherには決定的なアーキテクチャ上の欠陥があります。それは、未処理の例外が兄弟タスクをキャンセルしないことです。
gather()で1つのタスクが失敗した場合、他のタスクは「孤立した」コルーチンとしてバックグラウンドで実行を続け、メモリを消費し、データベース接続を開放したままにし、サイレントなバグを生成します。
レガシーな問題: asyncio.gatherにおける孤立したタスク
import asyncio
async def fetch_user(user_id: int):
await asyncio.sleep(0.5)
if user_id == 2:
raise ValueError("User database connection timeout!")
return {"id": user_id, "name": f"User {user_id}"}
async def fetch_orders(user_id: int):
# This keeps running and executing even after fetch_user fails!
await asyncio.sleep(2.0)
print("Orders fetched (wasted database compute!)")
return ["Order_101", "Order_102"]
async def main_legacy():
try:
results = await asyncio.gather(
fetch_user(2),
fetch_orders(2)
)
except ValueError as e:
print(f"Caught error: {e}")
# fetch_orders is still running in the background!
現代の解決策: asyncio.TaskGroup (Python 3.11以降)
Python 3.11では、asyncio.TaskGroupを介した構造化並行処理が導入されました。非同期コンテキストマネージャー(async with)内で使用すると、グループ内のいずれかのタスクが例外を発生させた場合、TaskGroupは残りのすべての兄弟タスクを自動的にキャンセルし、リソースをクリーンアップして、ExceptionGroupを発生させます。
import asyncio
async def fetch_user(user_id: int):
await asyncio.sleep(0.5)
if user_id == 2:
raise ValueError("User database connection failed!")
return {"id": user_id, "name": f"User {user_id}"}
async def fetch_orders(user_id: int):
try:
await asyncio.sleep(2.0)
return ["Order_101", "Order_102"]
except asyncio.CancelledError:
print("fetch_orders was cleanly cancelled because fetch_user failed!")
raise
async def main_modern():
try:
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(fetch_user(2))
task2 = tg.create_task(fetch_orders(2))
# Execution reaches here only if both tasks succeed
print(task1.result(), task2.result())
except* ValueError as eg:
# Python 3.11+ exception group pattern matching
for error in eg.exceptions:
print(f"Handled error in TaskGroup: {error}")
if __name__ == "__main__":
asyncio.run(main_modern())
枢機卿の罪: イベントループのスタベーションとその解決策
asyncioのイベントループは単一のスレッドで実行されるため、同期ブロッキング呼び出しはアプリケーション全体をフリーズさせます。エンドポイントがCPU負荷の高いループを実行したり、time.sleep()を呼び出したり、asyncpgの代わりにpsycopg2のような同期データベースドライバーを使用したりすると、そのサーバーで待機している他のすべての同時ユーザーがブロックされます。
スタベーションのデモンストレーション
import asyncio
import time
# WRONG: Synchronous blocking call inside async function!
async def bad_endpoint():
# Freezes the ENTIRE event loop for 2 full seconds!
# No other requests can be accepted or processed during this window.
time.sleep(2.0)
return {"status": "ok"}
解決策1: asyncio.to_threadによるブロッキング呼び出しのオフロード
同期サードパーティSDK(AWS S3用のboto3や画像処理用のPIL/Pillowなど)の場合、asyncio.to_thread()を使用します。これにより、イベントループを停止させることなく、ブロッキング呼び出しをPythonの内部スレッドプールに委譲します。
import asyncio
from PIL import Image
def resize_image_sync(filepath: str, output_path: str):
# CPU-heavy image compression
with Image.open(filepath) as img:
img.thumbnail((800, 800))
img.save(output_path, "JPEG", quality=85)
async def handle_image_upload(filepath: str, output_path: str):
# Cleanly offloaded to background thread pool
await asyncio.to_thread(resize_image_sync, filepath, output_path)
return {"status": "optimized"}
本番環境パターン: asyncio.Semaphoreによる並行処理の制限
APIのクロールや数千のキューアイテムの処理を行う際、制限なしに数千のコルーチンを同時に生成すると、外部サービスを圧倒したり、ファイルディスクリプタを使い果たしたり、HTTP 429 Too Many Requestsエラーを引き起こしたりする可能性があります。並行処理を制限するにはasyncio.Semaphoreを使用します。
import asyncio
import aiohttp
class BoundedAPIScraper:
def __init__(self, max_concurrent_requests: int = 25):
self.semaphore = asyncio.Semaphore(max_concurrent_requests)
self.session: aiohttp.ClientSession | None = None
async def __aenter__(self):
self.session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self.session:
await self.session.close()
async def fetch_item(self, item_id: int) -> dict:
async with self.semaphore:
# At most 25 requests will enter this block concurrently
url = f"https://api.example.com/v1/items/{item_id}"
assert self.session is not None
async with self.session.get(url) as response:
return await response.json()
async def process_all(self, item_ids: list[int]) -> list[dict]:
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(self.fetch_item(i)) for i in item_ids]
return [t.result() for t in tasks]
イベントループのスループット向上: uvloop
デフォルトのPythonイベントループ(asyncio.SelectorEventLoop)は純粋なPythonで書かれています。本番環境のLinuxサーバーでは、これをuvloop(Node.jsの基盤となるlibuv Cライブラリの上にCythonで書かれたドロップイン置換)に置き換えることで、I/Oスループットが2倍から4倍に向上し、PythonのネットワークパフォーマンスがNode.jsやGoと直接肩を並べるようになります。
uv add uvloop
import asyncio
import sys
# Configure uvloop as the default event loop policy on Unix
if sys.platform != "win32":
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
async def main():
print("Running on ultra-fast libuv event loop!")
if __name__ == "__main__":
asyncio.run(main())
こちらもおすすめ
- 2026年のPython並行処理: AsyncIO vs スレッド vs プロセス
- 本番環境での非同期Pythonメモリリークのプロファイリング
- 高並行性向けPython FastAPIの最適化
- FastAPI vs Litestar (2026): パフォーマンス、ベンチマーク、そして切り替え時
よくある質問
コルーチンは、async defで定義され、awaitされたときに制御を譲る関数です。Futureは、まだ準備ができていない最終的な結果を表す低レベルのオブジェクトです。タスクは、コルーチンをラップし、即座のバックグラウンド実行のためにイベントループにスケジュールするFutureの具体的なサブクラスです。
TaskGroupは構造化並行処理を実装しています。子タスクのいずれかが未処理の例外で失敗した場合、TaskGroupは残りのすべての兄弟タスクを自動的にキャンセルし、ExceptionGroupを発生させます。レガシーなasyncio.gatherは、兄弟タスクを切り離された孤立したコルーチンとして実行し続けます。
CPU負荷の高いループやブロッキング同期呼び出しを非同期関数内で直接実行しないでください。asyncio.to_thread()を使用して、同期I/Oまたは高速CPU処理をオフロードします。重い継続的な計算(データ処理やモデル推論など)には、ProcessPoolExecutorまたはCeleryのような専用のタスクキューを使用してください。
いいえ。GILは、複数のCPUバウンドスレッドが複数のコアでPythonバイトコードを並行して実行するのを制限するだけです。I/Oバウンドなネットワークワークロード(HTTPリクエスト、データベース読み取り、WebSocketストリーミング)の場合、スレッドはネットワークソケットを待機している間にGILを解放するため、asyncioは単一のコアで数万の同時接続を効率的に処理できます。
知識チェック
関連ガイドと詳細解説
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

2026年のPython並行処理:AsyncIO、スレッド、プロセス
CPUコードが4スレッドで50%遅くなる理由、AsyncIOイベントループ、GIL回避策、マルチプロセシングのベンチマークをアーキテクチャ決定木と比較して解説します。
Read more
PythonコードベースをフリースレッドCPython 3.13へ移行する
C拡張機能を監査し、GILの前提を排除して、マルチスレッドPythonワークロードをフリースレッドCPython 3.13以降へ安全に移行し、真の並列処理を実現しましょう。
Read more
本番環境のSQLite: WALモード、高並行性、そして実践的なPRAGMA設定
高スループットな本番環境でSQLiteをマスターしましょう。先行書き込みログ(WAL)、busy_timeoutのチューニング、読み書きの同時実行性、そして実用的なベンチマークについて解説します。
Read more