🦔
初心者でも分かるPythonのasyncio入門:async/awaitを料理で理解する非同期まとめ
概要
Pythonコードを見ているとasyncをよく見かけますが、これは「非同期処理を行う関数である」ことを示すキーワードです。
改めて、非同期処理が果たす役割や利点について整理していきます。
asyncの役割
非同期処理を可能にする。並列ではなく、タスク切替という「並行」処理
- async
非同期関数(中断と再開ができる関数)を定義する - await
ここで「待ち」が発生するので、他のタスクへ切り替えてOKというポイント
料理に例えると
同期処理
- プロセス
米を洗う → 炊飯してずっと待つ → ご飯混ぜる → カレー作る → 完成 ↑ この間ずっと手が止まる - 実行時間
「炊飯(40秒) → 煮込み(10秒)」の順で必ず直列になるので、約50秒 - コード
from time import sleep def ライス準備(): print("米を洗う") print("炊飯器で炊くボタンを押す") sleep(40) # 炊きあがるまでは待機。CPUはブロッキング状態 print("米を混ぜる") def カレー準備(): print("野菜を切る") print("肉を切る") print("具材を炒める") print("ルーを溶かす") sleep(10) # 煮込み終わるまでは待機。CPUはブロッキング状態 print("煮込み完了") def main(): # ライス準備完了後にカレー作りをスタート ライス準備() カレー準備() print("ライスとカレーを皿に盛り付ける") print("カレーライス完成!") main() # 実行結果 # 米を洗う # 炊飯器で炊くボタンを押す # 米を混ぜる # 野菜を切る # 肉を切る # 具材を炒める # ルーを溶かす # 煮込み完了 # ライスとカレーを皿に盛り付ける # カレーライス完成!
非同期処理
- プロセス
米を洗う → 炊飯開始(await) ↓(炊飯待ちの間イベントループがカレーへ) カレー作る(並行) ↓(煮込み中は再び他のタスクへ戻る) 炊飯完了 → ご飯を混ぜる → 完成 - 実行時間
「炊飯(40秒)」「煮込み(10秒)」が並行で進むので、約40秒 - コード
import asyncio async def ライス準備(): print("米を洗う") print("炊飯器で炊くボタンを押す") await asyncio.sleep(40) # 炊きあがるまでは待機。CPUは非ブロッキング状態(OSタイマーなので外部I/O待ちではない。I/O待ちの疑似表現) print("米を混ぜる") async def カレー準備(): print("野菜を切る") print("肉を切る") print("具材を炒める") print("ルーを溶かす") await asyncio.sleep(10) # 煮込み終わるまでは待機。CPUは非ブロッキング状態(OSタイマーなので外部I/O待ちではない。I/O待ちの疑似表現) print("煮込み完了") async def main(): # ご飯炊きとカレー作りを “同時スタート” ライス_task = asyncio.create_task(ライス準備()) カレー_task = asyncio.create_task(カレー準備()) # どちらも終わるまで待つ await asyncio.gather(ライス_task, カレー_task) print("ライスとカレーを皿に盛り付ける") print("カレーライス完成!") asyncio.run(main()) # 実行結果 # 米を洗う # 炊飯器で炊くボタンを押す # 野菜を切る # 肉を切る # 具材を炒める # ルーを溶かす # 煮込み完了 # 米を混ぜる # ライスとカレーを皿に盛り付ける # カレーライス完成!
await asyncio.gather(...)
複数の「awaitable(コルーチン / Task / Future)」をまとめて並行実行し、全て完了するのを待機。結果を順序通りに返す
- 同時実行: 引数の「awaitable」を(基本)同時に走らせる
- 合流: すべて完了するまで待つ
- 戻り値: 引数の順序と同じ順序で結果のタプル(または値)を返す
- 例外: オプションreturn_exceptionsのTrue/Falseで設定する
メリット
ネットワークやファイルの読み書きの“待ち時間”を有効活用することによって、全体の処理を高速化する
API呼び出しやDBアクセス等は外部応答が遅いため、CPUの待ち時間を活用して別の処理を進められる
向いていないもの
CPU集約処理(重いループ計算)は、asyncでは速くならない
※ CPU集約処理は、スレッド/プロセスプールに逃がす
仕組み
コルーチン関数(途中で止めたり再開したりできる関数)
# コルーチン関数は、async defで定義
async def fetch_data():
...
- 通常の関数(呼び出されたら最後まで一気に実行される)と違い、中断と再開ができる
- awaitに到達したら、待っている間に他のタスクへ切り替える
- 外部のI/O完了イベントが届くと、処理が再開される
await(明示的な待機ポイント)
# CPU待機ポイントを明示的に指定
await session.get("https://api.example.com/data")
- 「ここでI/Oを待つから、その間に他処理を進めてOK」という協調的な切り替えポイント
イベントループ(中断と再開を管理する司令塔)
import asyncio
async def main():
await asyncio.sleep(0.1) # 0.1秒待機という外部処理完了後にmain処理再開
return "done"
# 新しいイベントループを作成し、コルーチン関数である「main()」を最後まで実行+戻り値を返し、ループを安全にクローズ
result = asyncio.run(main())
print(result)
# ->「done」が出力される
- 「今I/O待ちが発生した → 他のタスクへ」「終わった → 再開」という切り替えを管理
-
asyncio.run()はネスト不可(既にイベントループが動いている環境では使えない)
非同期処理を使用する場面
- 大量のHTTPリクエスト
- WebSocket通信、チャット、通知
- DB/キャッシュへの非同期アクセス
- ファイルI/Oが多いETL/ログ処理(対応ライブラリ前提)
- タイマーやスケジューリング(一定間隔でのポーリングetc.)
Discussion