🦔

初心者でも分かる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