今回はPythonの非同期処理について説明します。
同期処理と非同期処理のイメージ
同期処理と非同期処理の違いは、順次実行か並行実行かです。
同期処理が各タスクが順番で実行され、非同期処理が各タスクが並行で実行されます。
同期処理は書いたプログラムの通りの順番で処理が実行されていくという点である程度イメージしやすいのですが、非同期処理はどういう技術で実行されるのか個人的にイメージしずらい部分がずっとありました。
なので理解を深めるために調査して、この記事を書くに至ったわけです。
非同期処理の技術的要素をPythonで説明
- コルーチン
- await
- イベントループ
- タスク
- 同期プリミティブ
コルーチン
コルーチンとは、途中で処理を中断したり再開したりできる性質の関数です。
普通の関数(サブルーチン)は「呼び出されたら最後まで一気に実行」されますが、コルーチンは「一時停止 → 他の処理に切り替え → また戻って再開」ができます。
Python では async def で定義した関数が「コルーチン関数」になります。
import asyncio
# コルーチン関数
async def say_hello():
print("Hello World")
# 実行
asyncio.run(say_hello())
↓↓出力結果↓↓
Hello World
Pythonには同期関数(普通の関数)を非同期関数として扱えるように変換する仕組みがあり、それがsync_to_asyncといいます。
Pythonのasyncioでは、基本的に非同期関数(async def)しかawaitできません。
ですが実際のアプリケーションでは、同期的にしか書かれていない処理(例:DjangoのDBアクセス、ファイル操作、CPU計算など)を呼びたい場面が多いです。
そこで sync_to_async を使うと、同期関数を「非同期っぽく」ラップして、イベントループの中で安全に呼べるようになります。
import asyncio
from asgiref.sync import sync_to_async
def sync_add(x, y):
# 普通の同期関数
return x + y
async def main():
# 同期関数を非同期的に呼び出す
result = await sync_to_async(sync_add)(1, 2)
print(result)
asyncio.run(main())
↓↓出力結果↓↓
3
await
awaitとは、「非同期処理の結果が返ってくるまで一時停止する」ための演算子です。
ただし「プログラム全体を止める」のではなく、その関数だけを中断して、他の処理に進ませることができます。
import asyncio
async def add(x, y):
print("add: start")
await asyncio.sleep(0.5) # 0.5秒間停止
print("add: end")
return x + y
async def multiply(x, y):
print("multiply: start")
z = await add(x, y) # addが終わるまで、この関数は一時停止
result = x * y + z
print("multiply: end")
return result
async def main():
ans = await multiply(2, 3)
print("result:", ans)
asyncio.run(main())
↓↓出力結果↓↓
multiply: start
add: start
add: end
multiply: end
result: 11
イベントループ
イベントループは、非同期処理の司令塔みたいなものです。
どのタスクをいつ動かすかを管理して、待っているタスクを止めて動けるタスクに切り替える役割を持っています。
asyncio.run() が内部でイベントループを作って動かします。
import asyncio
async def worker(name, sec):
print(f"{name} start")
await asyncio.sleep(sec)
print(f"{name} end")
return f"{name} result"
async def main():
results = await asyncio.gather(
worker("A", 2),
worker("B", 3),
worker("C", 1),
) # gatherは複数の非同期処理(コルーチンやタスク)をまとめて同時に実行し、その結果をまとめて返す関数
print("結果:", results)
asyncio.run(main())
↓↓出力結果↓↓
A start
B start
C start
C end
A end
B end
結果: ['A result', 'B result', 'C result']
複数のタスクを効率よく並行実行するために以下のような流れで動きます。
1. タスクの登録
async def で定義された関数を呼び出すと、非同期タスクが生成されます。
2. 非同期タスクの実行
イベントループはタスクキューから順番にタスクを取り出して実行します。
3. 待機中の別処理実行
タスクが await を使って何かを待っている間、イベントループは他のタスクを実行します。
4. 再開
待機が終わると、タスクは再びキューに戻されて実行が再開されます。
「3. 待機中の別処理実行」については、以下のようにCPUは暇だけど外部からの応答を待っているときに別処理を実行します。
・ネットワーク通信
APIにリクエストを送って、サーバーの応答を待つ
Webページをダウンロードする
・ファイル操作
大きなファイルを読み込む/書き込む
SSDやHDDからデータが返ってくるのを待つ
・データベースアクセス
SQLクエリを投げて、結果が返るまで待つ
・タイマー系
await asyncio.sleep(3) のように「時間が経つのを待つ」
タスク
タスクとは、コルーチンをイベントループに登録したもので実行中のものを指します。
asyncio.create_task(coroutine)を使うと、コルーチンがタスクに変換され、イベントループでスケジューリングされます。
import asyncio
async def worker(name, sec):
print(f"{name} start")
await asyncio.sleep(sec)
print(f"{name} end")
async def main():
coroutine1 = worker("A", 2) # コルーチンオブジェクト
coroutine2 = worker("B", 3) # コルーチンオブジェクト
t1 = asyncio.create_task(coroutine1) # タスクに変換して実行開始
t2 = asyncio.create_task(coroutine2) # タスクに変換して実行開始
await t1 # タスクの完了を待つ
await t2 # タスクの完了を待つ
asyncio.run(main())
↓↓出力結果↓↓
A start
B start
(2秒後) A end
(3秒後) B end
同期プリミティブ
同期プリミティブは、複数のタスクが同時に動くときリソースの取り合いを防ぐための仕組みです。
非同期処理では複数のタスクが同時に動きます。
例えば、複数のタスクが同じファイルやデータベースにアクセスすると、競合や破損が起こる可能性があります。
そこで「同時にアクセスさせない」「順番に処理させる」ために使うのが同期プリミティブです。
同期プリミティブにはasyncio.Lock、asyncio.Semaphore、asyncio.Queueなどがありますが、以下のコードはasyncio.Lockを使った実例になります。
import asyncio
# Lockオブジェクトを作成(1つのタスクだけが同時に入れる)
lock = asyncio.Lock()
# 共有リソースにアクセスする関数
async def critical_section(name):
print(f"{name} が入室を希望")
async with lock: # ロックを取得(他のタスクはここで待機)
print(f"{name} が入室")
await asyncio.sleep(1) # 擬似的な処理時間
print(f"{name} が退室")
# 複数タスクを同時に起動
async def main():
await asyncio.gather(
critical_section("A"),
critical_section("B"),
critical_section("C")
)
asyncio.run(main())
↓↓出力結果↓↓
A が入室を希望
B が入室を希望
C が入室を希望
A が入室
A が退室
B が入室
B が退室
C が入室
C が退室
まとめ:並行処理を行うなら非同期処理を使う
非同期処理は並行処理を行うための技術です。
プログラムは基本的に書いたコードの順番に(上から下に)実行されますが、時には並行処理が必要なケースもあります。
ネットワーク通信(HTTPリクエスト、API呼び出し)やファイルの読み込み、DBアクセスを行うときがそのようなケースに当てはまるでしょう。
こういうケースの時に非同期処理が必要になり、よく理解しておかないと開発するときに躓くため基礎はしっかり押さえておく必要があります。
また今回は基礎的な内容だけを紹介しましたが、Pythonには非同期処理のための機能が複数用意されています。
全て理解するためにすべてを説明したいところですが、今回は非同期処理の基礎を理解するにとどめておきます。
