概要(asyncio は「1人で同時進行する」仕組み)
マルチスレッドやマルチプロセスは「人を増やす」イメージでした。
asyncio はちょっと違っていて、
「1人なんだけど、待ち時間のあいだに別の作業にパパっと切り替える」
というスタイルの仕組みです。
特に効いてくるのは、
- Web API を大量に叩く
- ソケット通信・WebSocket・チャットBOT
- データ収集のように、「待ち」がとにかく多い処理
です。
CPU をゴリゴリ使う計算を速くする魔法ではありません。
「待ってる時間をムダにしない」が asyncio の本質です。
基礎イメージ(マルチスレッドとの違いから入る)
マルチスレッドと async/await の違い
マルチスレッドは、OS レベルで「本当に同時に動きうる」流れを複数持ちます。
スレッドごとに実行のタイミングは OS に任せているので、「いつどこで切り替わるか」がコードからは見えにくい。
asyncio は、Python の中に「イベントループ」という“司令塔”を用意して、
- ある処理が「待ち」に入ったら、自分から
awaitで「今ヒマだから、他の仕事どうぞ」と言う - 司令塔が「じゃ、別の処理を再開させるね」と切り替える
というルールで動きます。
大事なポイントは、
- どこで切り替わるかを
awaitでコード側が明示するので、流れを追いやすい - 1つのスレッド(1人)でたくさんの I/O をさばける
- スレッドよりも「共有データ」「ロック」のややこしさが少ない
というメリットがある一方で、
- async/await の書き方に慣れるまで少し時間がかかる
- 非同期対応していないライブラリ(普通の requests など)は、そのままだと使いにくい
という側面もあります。
最小サンプルで「async/await の雰囲気」をつかむ
超シンプルな「待つだけ」の非同期処理
まずは、ネットワークもファイルも使わずに、asyncio.sleep だけで雰囲気をつかんでみます。
import asyncio
async def worker(name: str, delay: int):
print(f"{name} 開始")
await asyncio.sleep(delay)
print(f"{name} 終了({delay}秒待った)")
async def main():
task1 = asyncio.create_task(worker("A", 2))
task2 = asyncio.create_task(worker("B", 3))
await task1
await task2
asyncio.run(main())
Pythonここでいったん、行ごとの意味を丁寧に追います。
async def worker(...)
この関数は「非同期関数(コルーチン)」になります。
中で await を使えるのは、async def の中だけです。
await asyncio.sleep(delay)
「delay秒寝ているあいだ、他の処理に CPU を譲ります」の意味です。
ここでイベントループが「じゃあ他のタスク動かすね」と切り替えます。
asyncio.create_task(worker(...))
イベントループに「この非同期関数をタスクとして登録してね」と依頼します。
戻り値は Task オブジェクトで、「実行中の非同期処理のハンドル」です。
await task1
task1 の完了を待ちます。task1 がまだ動いているなら、イベントループは他のタスクを動かし続けます。
asyncio.run(main())
Python プログラムのエントリーポイントから、非同期世界を起動する儀式だと思ってください。
イベントループの作成~終了まで面倒を見てくれます。
このコードを動かすと、「A 開始」「B 開始」の後に、2秒後に A 終了、さらに1秒後に B 終了、のように「同時進行」している感じが分かるはずです。
典型例:非同期で Web API を大量に叩く
同じことを「同期版(ふつうの requests)」でやるとどうなるか
とりあえず、普通の同期コードからイメージしてみます。
import time
import requests
URLS = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/3",
]
def fetch(url: str) -> int:
resp = requests.get(url, timeout=10)
return resp.status_code
def sync_main():
start = time.time()
results = []
for url in URLS:
status = fetch(url)
results.append((url, status))
elapsed = time.time() - start
print("結果:", results)
print(f"同期処理 合計: {elapsed:.2f}秒")
if __name__ == "__main__":
sync_main()
Pythondelay/1、delay/2、delay/3 に順番にアクセスするので、だいたい 1+2+3 秒 ≒ 6 秒近くかかります。
非同期版:aiohttp を使って並行実行する
非同期 HTTP クライアントとしてよく使われるのが aiohttp です。
pip install aiohttp
これを使って、同じ URL リストを「asyncio で同時進行」させてみます。
import asyncio
import time
import aiohttp
URLS = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/3",
]
async def fetch(session: aiohttp.ClientSession, url: str) -> tuple[str, int]:
async with session.get(url, timeout=10) as resp:
return url, resp.status
async def async_main():
start = time.time()
async with aiohttp.ClientSession() as session:
tasks = [asyncio.create_task(fetch(session, url)) for url in URLS]
results = await asyncio.gather(*tasks)
elapsed = time.time() - start
print("結果:", results)
print(f"非同期処理 合計: {elapsed:.2f}秒")
if __name__ == "__main__":
asyncio.run(async_main())
Pythonここが asyncio で一番よく使うパターンです。
async with aiohttp.ClientSession()
セッション(接続の管理)を非同期対応で扱うための文です。
「このブロックの間だけ session を使う」と覚えてOKです。
tasks = [asyncio.create_task(fetch(...)) for url in URLS]
URL ごとに fetch のタスクを作り、「全部イベントループに登録する」。
await asyncio.gather(*tasks)
複数タスクをまとめて待ち、結果をリストで返します。
タスクの順番と結果の順番が対応します。
delay/1、delay/2、delay/3 は「同時にリクエストが飛ぶ」ので、
合計時間はだいたい 3 秒ちょっとで収まるはずです。
これが「待ち時間を重ねない」非同期の強さです。
asyncio でつまずきやすいポイントを深掘りする
1. 「async 関数は呼ぶと“すぐ実行されない”」という感覚
ここでよく混乱が起きます。
普通の関数は、func() と呼んだ瞬間に中身が実行されます。
でも、async def で定義した関数は、func() と呼ぶと「コルーチンオブジェクト」が返ってきます。
これは「まだ動いていない、実行予定の処理」です。
このコルーチンを本当に実行するには、
await func()とするasyncio.create_task(func())でタスクにして、イベントループに渡す
のどちらかが必要です。
例:
async def f():
print("実行")
coro = f() # まだ実行されていない
await coro # ここで初めて「実行」
Pythonここを理解していないと、「async 関数を呼んだのに動かない」「警告が出る」みたいな現象になります。
2. 同期コードと非同期コードを混ぜるときの注意
asyncio の世界の中で、ブロッキングな処理(普通の requests や time.sleep など)をそのまま呼ぶと、
「イベントループごと止まる」ので意味がなくなります。
非同期関数の中で、
time.sleep()ではなくawait asyncio.sleep()を使う- HTTP は requests ではなく aiohttp、DB は対応ライブラリ(asyncpg など)を使う
という風に、「非同期対応の I/O を使う」ことが重要です。
どうしても同期関数を呼びたい場合は、
asyncio.to_thread()(Python 3.9+)で別スレッドに逃がす
という方法もあります。
import asyncio
import time
def blocking_task():
time.sleep(3)
return "done"
async def main():
print("start")
result = await asyncio.to_thread(blocking_task)
print("result:", result)
asyncio.run(main())
Pythonこうすると、「イベントループは止めずに、ブロッキング処理だけスレッドでやってもらう」ことができます。
ただし、これを多用しすぎると「結局スレッド地獄」になるので、
基本は「最初から非同期対応のライブラリを選ぶ」方向がおすすめです。
3. エラーとキャンセルの扱い
asyncio.gather はデフォルトでは、どれかのタスクが例外を出すとそこで例外を投げます。
他のタスクはキャンセルされます。
これが嫌で、「エラーはエラーで個別に扱いたい」場合は、return_exceptions=True を指定します。
results = await asyncio.gather(*tasks, return_exceptions=True)
for r in results:
if isinstance(r, Exception):
print("エラー:", r)
else:
print("成功:", r)
Pythonまた、タスクを途中で止めたい場合は task.cancel() を使いますが、
キャンセルも例外(asyncio.CancelledError)として扱われるため、
タスク側で try: ... except asyncio.CancelledError: ... のように処理することがあります。
初心者のうちは、まずは「エラーが出たらログを出して落ちる」で良いので、
本番運用で徐々にキャンセルや個別エラー処理を覚えていく感じで大丈夫です。
自動化の中で asyncio をどう使うか(具体イメージ)
Web API 集計 BOT を asyncio 化するとどうなるか
すでに「Web API 集計 BOT」を学んでいる前提で話すと、
あの「1日ぶんのデータを API から取得して集計する」処理を、
日付ごと・パラメータごとに asyncio で並行に回せるわけです。
例えば、
「2025-01-01~2025-01-07 までの 7 日ぶんのデータを、順番ではなく同時に取得したい」
といったときに、
- 同期版:for ループで 7 回 requests.get → トータル時間はほぼ 7 日ぶんの合計
- 非同期版:7 日分の fetch を asyncio.gather で同時並行 → いちばん遅い日付に合わせた時間
という差が出ます。
データ収集 BOT とフォルダ監視の組み合わせ
フォルダ監視(watchdog)や cron と組み合わせて、
- 新しいジョブが来たら、非同期で複数の外部サービスから情報を引っ張ってくる
- 非同期で API を叩きつつ、ローカルの I/O も並行で進める
みたいな構成も作れます。
ここで大事なのは、
- 「今の処理は本当に async にする意味があるか?」
- 「同期版(concurrent.futures)で十分ではないか?」
を一度考えることです。
asyncio は強力ですが、「何もかも async 化する」のはおすすめしません。
ネットワークやソケットをガッツリ扱うところに絞って使うと、
コード量に対して得られるメリットが大きくなります。
まとめ(asyncio は「待ち時間を並列化する設計思想」)
ここまでのポイントをぎゅっとまとめると、こうなります。
asyncio は、「待ち時間(I/O)だらけの処理」を、1つのスレッドで効率的にさばくための仕組み。async def で非同期関数を定義し、その中で await することで「ここで他の仕事に切り替えていいよ」と宣言する。
非同期 HTTP には aiohttp、DB には async 対応ライブラリなど、「非同期前提の I/O ライブラリ」を使うことが重要。
複数タスクを並行実行する基本パターンは、asyncio.create_task + asyncio.gather。
同期コードと混ぜるときは、asyncio.to_thread などでブロッキング処理をイベントループから分離する。
最初は頭が少し混乱すると思います。
でも、「asyncio は“スレッドの代わり”ではなく、“待ち時間をうまく回す設計”なんだ」と理解できると、一気に見通しが良くなります。
