イベントループとタスク | asyncioの利用 | Python本格超入門

スポンサーリンク
スポンサーリンク
amazon
スマイルSALE
--:--:--
ad. 価格範囲を指定して商品を探せます

イベントループとタスク

Pythonで効率的な並行処理を実現するための仕組みが非同期処理です。その中心的な役割を担うのが asyncio モジュールであり、その核となる概念が「イベントループ」と「タスク」です。

イベントループは、複数の非同期処理をスケジュールし、CPUの空き時間を無駄にすることなく効率的に実行を管理する司令塔のような役割を果たします。この記事では、イベントループとタスクの仕組み、そして基本的な操作方法について解説します。

イベントループの役割と基本動作

イベントループは、プログラムの裏側で常に稼働し、実行待ちのタスクを監視して、次にどの処理を進めるかを瞬時に決定するループ機構です。ネットワーク通信の待ち時間(I/Oバウンドな処理)が発生すると、別のタスクに自動で処理権を切り替えることで、全体の処理効率を引き上げます。

Python 3.7以降では、最もシンプルな起動方法として asyncio.run() が提供されています。

import asyncio

# async def で非同期関数(コルーチン)を定義
async def say_hello():
    print("Hello, world!")
    # 1秒間、非同期的に待機(この間に他の処理を実行できる)
    await asyncio.sleep(1)
    print("Goodbye, world!")

# イベントループを自動で起動し、関数を実行
asyncio.run(say_hello())

この例では、say_hello という非同期関数(コルーチン)を呼び出しています。await asyncio.sleep(1) が実行されると、一時的に処理が中断され、イベントループに制御が戻ります。今回はタスクが1つだけなので単に1秒待ちますが、複数のタスクがある場合にこの仕組みが真価を発揮します。

タスクの作成と並行実行

複数の非同期処理を「同時に(並行して)」実行したい場合は、コルーチンをそのまま await するのではなく、asyncio.create_task() を使ってイベントループにタスクとして登録します。

import asyncio

async def say_hello(name):
    print(f"Hello, {name}!")
    await asyncio.sleep(1)
    print(f"Goodbye, {name}!")

async def main():
    # コルーチンをタスクとしてスケジュール(登録した時点で即座に実行が開始される)
    task1 = asyncio.create_task(say_hello("Alice"))
    task2 = asyncio.create_task(say_hello("Bob"))

    # それぞれのタスクが完了するのを待つ
    await task1
    await task2

asyncio.run(main())

asyncio.create_task() を呼び出すと、関数はバックグラウンドで即座に実行対象となります。task1await asyncio.sleep(1) で待機状態に入ると、イベントループはすかさず task2 の処理を進めます。その結果、本来なら2秒かかる処理が、並行して処理されることで約1秒で完了します。

イベントループの明示的な操作

通常は asyncio.run() を使えば十分ですが、既存のアプリケーションに非同期処理を組み込む場合や、より細かなライフサイクル管理を行いたい場合は、イベントループを明示的に取得・操作することがあります。

import asyncio

async def say_hello():
    print("Hello, world!")
    await asyncio.sleep(1)
    print("Goodbye, world!")

# 現在のスレッドのイベントループを取得
loop = asyncio.get_event_loop()

# ループ上で動くタスクを作成
task = loop.create_task(say_hello())

# 指定したタスクが完了するまでループを走らせる
loop.run_until_complete(task)

# ループを安全に閉じる
loop.close()

loop.run_until_complete() を使用すると、引数に渡したタスクが終了するまでイベントループを駆動させ、終了と同時にループの実行を止めます。低レベルなAPIを直接触る必要がある拡張ライブラリの開発や、複雑なイベント駆動型システムを設計する際に役立つ手法です。

注意点:同期処理によるブロッキングの防止

非同期処理を実装する上で最も注意しなければならないのが、「イベントループを決して止めてはならない」という原則です。非同期関数(async def)の内部で、通常の time.sleep() や、大規模なファイル入出力、重い計算処理などの「同期的なブロッキング処理」を実行してしまうと、イベントループ全体の動きがストップし、他のすべてのタスクも道連れで停止してしまいます。

どうしても同期的な重い処理を実行する必要がある場合は、loop.run_in_executor() などを利用して、別スレッドや別プロセスに処理を逃がすような設計を心がけましょう。

まとめ

Pythonの非同期処理において、イベントループとタスクの重要ポイントは以下の通りです。

  • イベントループは非同期処理の司令塔:複数のタスクを監視し、待ち時間が発生したタスクから別のタスクへ実行権を自動で切り替えることで、全体の処理を効率化します。
  • asyncio.run()による自動管理:手動でループを構築・クローズする必要がなく、エントリーポイントとなる非同期関数を渡すだけで安全に実行できます。
  • asyncio.create_task()で並行処理:コルーチンをタスク化してイベントループに登録することで、バックグラウンドでの即時・並行実行が可能になり、I/O待ちの多い処理を劇的に高速化できます。
  • ブロッキング処理の徹底排除:非同期関数内で time.sleep() などの同期的な処理を行うと、ループ全体が停止するため、必ず asyncio.sleep() などの非同期APIを使用する必要があります。

次のステップとして、多数のタスクをよりスマートに一括管理して結果を回収できる asyncio.gather() などの便利なAPIも学んでいくと、さらに実践的なコードが書けるようになります。