Python非同期 vs スレッド: asyncioを使うべき時

Backend2026-09-11TryQuickToolBox

Pythonで並行処理が重要な理由

複数のAPIからデータを取得するPythonスクリプトがあるとします。動作はしますが遅い——各リクエストが前のリクエストの完了を待っているからです。asyncioとスレッドについて聞いたことがあるかもしれませんが、どちらを使うべきでしょうか?間違ったモデルを選ぶと、不必要に複雑なコードか、ひどく遅いコードになってしまいます。

このガイドはその混乱を解消します。Pythonのasyncとスレッディングモデルを比較し、それぞれをいつ使うべきかを正確に示し、自分のプロジェクトに応用できる実践的な例を提供します。

Pythonの並行処理の全体像を理解する

Pythonには主に3つの並行処理モデルがあります:

GILは複数のスレッドが同時にPythonバイトコードを実行するのを防ぐミューテックスです。つまりスレッドはCPUバウンドのタスクを高速化しませんが、I/O操作中はGILが解放されるため、I/Oバウンドのタスクには役立ちます。

asyncioを使うべき時

asyncioは、プログラムが外部イベント——ネットワークリクエスト、ファイルI/O、データベースクエリ、タイマー——の待機にほとんどの時間を費やす場合に真価を発揮します。単一スレッドを使用するため、スレッド作成やコンテキストスイッチのオーバーヘッドを避けられます。イベントループは何千もの同時接続を効率的に管理します。

次の場合にasyncioを使いましょう:

複数のURLを同時に取得する簡単な例を次に示します:

import asyncio
import aiohttp

async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

async def main():
    urls = [
        'https://example.com',
        'https://example.org',
        'https://example.net',
    ]
    tasks = [fetch(url) for url in urls]
    results = await asyncio.gather(*tasks)
    print(f'Fetched {len(results)} pages')

asyncio.run(main())

スレッドを使うべき時

スレッドは、async版が存在しないブロッキングI/O操作がある場合や、asyncに対応していないレガシーコードを扱う場合に適しています。また、少数のタスクであれば理解しやすいという利点もあります。

次の場合にスレッドを使いましょう:

concurrent.futures.ThreadPoolExecutorを使った例:

from concurrent.futures import ThreadPoolExecutor
import requests

def fetch(url):
    response = requests.get(url)
    return response.text

urls = [
    'https://example.com',
    'https://example.org',
    'https://example.net',
]

with ThreadPoolExecutor(max_workers=10) as executor:
    results = list(executor.map(fetch, urls))

print(f'Fetched {len(results)} pages')

asyncioとスレッドの比較

判断の参考になるよう、並べて比較してみましょう:

項目 asyncio スレッド
並行処理モデル 単一スレッドのイベントループ 複数のOSスレッド
最適な用途 I/Oバウンド、高い並行性 I/Oバウンド、ブロッキング呼び出し
CPUバウンドの性能 低い(GIL) 低い(GIL)
スケーラビリティ 数千のタスク 数百のスレッド
複雑さ async/await構文が必要 慣れ親しんだ同期スタイル
デバッグ トレースバックで難しい場合がある 標準的なデバッグツール
エコシステム asyncサポートが拡大中 普遍的

CPUバウンドのタスクはどうする?

GILのため、asyncioもスレッドもPythonでCPU集約的な処理を並列化する助けにはなりません。数値計算、画像処理、データ圧縮などのCPUバウンドのタスクには、multiprocessingまたはconcurrent.futures.ProcessPoolExecutorを使いましょう。

例:

from concurrent.futures import ProcessPoolExecutor

def cpu_heavy(n):
    return sum(i * i for i in range(n))

if __name__ == '__main__':
    with ProcessPoolExecutor() as executor:
        results = list(executor.map(cpu_heavy, [10**6, 10**6, 10**6]))
    print(results)

ベストプラクティスと落とし穴

FAQ

asyncioとスレッドを一緒に使えますか?

はい。asyncio.to_thread()またはloop.run_in_executor()を使って、ブロッキングコードをスレッドプールで実行できます。これは、イベントループをブロックせずにasyncコードから同期ライブラリを呼び出す必要がある場合に便利です。

asyncioはGILと一緒に動作しますか?

はい、asyncioは単一スレッドで実行され、GILの影響を受けます。ただし、I/Oバウンドのタスク向けに設計されているため、I/O操作中はGILが解放され、他のタスクが実行できます。CPUバウンドの処理では、asyncioは速度向上をもたらしません。

asyncioとスレッド、どちらが速いですか?

多くの並行操作を伴うI/Oバウンドのタスクでは、asyncioはスレッドのオーバーヘッドを避けるため、一般的に高速でスケーラブルです。少数のブロッキングタスクでは、スレッドの方が簡単で同様の性能を発揮する場合があります。どちらもCPUバウンドのタスクには役立ちません。

正しい選択をするために

まず、ボトルネックがI/OかCPUかを特定することから始めましょう。I/Oで高い並行性が必要ならasyncioを選びましょう。I/Oだがブロッキングライブラリや単純なスクリプトを扱うなら、スレッドは堅実な選択です。CPUバウンドの処理にはマルチプロセッシングを使いましょう。

これらのモデルを組み合わせることもよくあることを覚えておきましょう——たとえば、ネットワーク操作にasyncioを使い、ファイルI/Oにスレッドプールを使うなどです。重要なのはトレードオフを理解し、特定の問題に合ったツールを選ぶことです。

並行API呼び出しから返されたJSONデータを素早く整形または検証したい場合は、JSON Formatterを試して、簡単に整形とデバッグを行いましょう。