•13 min read

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

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

Pythonにおける非同期プログラミングは、Python 3.4での実験的なアドオンから、現代の高性能マイクロサービス、APIゲートウェイ、ストリーミングデータパイプラインにおけるデフォルトのアーキテクチャパラダイムへと成熟しました。FastAPI、Litestar、Sanicのようなフレームワークに支えられ、Pythonのバックエンドは数万の同時接続を日常的に処理しています。

しかし、広く採用されているにもかかわらず、asyncioはPythonで最も誤解されているシステムの一つです。開発者は頻繁にイベントループのスタベーションを引き起こしたり、非同期実行コンテキスト内で同期ブロッキング呼び出しを混在させたり、適切な例外分離なしにasyncio.gatherのような古いレガシーAPIに依存したりしています。

この詳細な解説では、Pythonのasyncioランタイムが内部でどのように機能しているのかを解き明かし、asyncio.TaskGroupとレガシーなgatherを比較し、asyncio.to_threadを使ってCPUバウンドな作業を安全にオフロードする方法を実演し、高並行レート制限付きワーカーパイプラインを構築します。

Audio Briefing
0:00 / 0:00
Part of a Series

高性能Pythonバックエンドシリーズ

Part 4 of 4

イベントループアーキテクチャ: 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操作が準備できるまでイベントループに制御を戻すことができる、強化されたジェネレーターです。


Advertisement

現代の構造化並行処理: 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]

Advertisement

イベントループのスループット向上: 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())

こちらもおすすめ

よくある質問

コルーチンは、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は単一のコアで数万の同時接続を効率的に処理できます。


知識チェック


関連ガイドと詳細解説

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement