Pythonのasync/awaitを理解したい【asyncio】
用語
概念を理解するために、非同期処理で何が起きているかを把握します。
- イベントループ:タスクをスケジュールする
- タスク:コルーチンを実行し、実行結果などを管理する
- コルーチン:実行や一時停止ができる処理
私たちはやりたいことをコルーチンで書いてタスクにしてイベントループに入れるだけで、イベントループがうまいこと処理してくれる、というイメージです。
ここでいううまいこととは、CPUを使わないのに何秒もかかるタスクがあるときに、代わりに別のタスクにCPUを使わせる、といった具合です。
これらのことをするために、Pythonの標準ライブラリであるasyncioを使用します。
コルーチンを作成する(async)
コルーチンを作成するのは簡単で、defの前にasyncをつけるとコルーチン関数ができます。
# 通常の関数(同期関数)
def main():
print('hello world')
# コルーチン関数(非同期関数)
async def async_main():
print('hello async world')
通常の関数は以下の通りで、実行するとすぐに結果が返ってきます。
>>> main()
hello world
コルーチン関数は実行すると、コルーチンオブジェクトを返すようになります。
>>> async_main()
<coroutine object main at 0x123456789>
コルーチンを実行するにはasyncio.runをします。
>>> asyncio.run(async_main())
hello async world
コルーチンの完了を待つ(await)
コルーチンは一時停止ができる処理と書きました。何か別の処理が完了するまで待つ時にはawaitを使います。
1秒かかるhelloというコルーチンをmainで行うとします。その際、helloが完了するまで待つためにawaitをつけます。
async def hello():
print('I say,')
await asyncio.sleep(1) # 1秒かかる
print('hello')
async def main():
await hello() # helloの完了を待つ
これを実行すると以下のようになります。
>>> asyncio.run(main())
I say,
hello
一方で、awaitをつけないと単にコルーチンオブジェクトを生成しているだけなので何も起きません。
async def hello():
print('I say,')
await asyncio.sleep(1) # 1秒かかる
print('hello')
async def main():
hello() # awaitがない
これを実行するとhelloが待たれていないというエラーが出ます。
>>> asyncio.run(main())
RuntimeWarning: coroutine 'hello' was never awaited
最初にコルーチンはタスクとして実行されると書きました。実はコルーチンをawaitすると、内部的にタスクが作成されコルーチンが実行されます。
なお、awaitは一時停止をすることが目的なのでasyncの中でしか書けません。
一方で、asyncio.runは同期関数からコルーチンを実行することが目的なのでasyncの中では書けません。
タスクを作成する
コルーチンは明示的にタスクを作成して実行することもできます。
asyncio.create_taskを使うと、コルーチンオブジェクトをタスクオブジェクトにして実行できます。
async def hello():
print('I say,')
await asyncio.sleep(1) # 1秒かかる
print('hello')
async def main():
await asyncio.create_task(hello()) # helloタスクの完了を待つ
これを実行すると以下のようにコルーチンをawaitした時と同じ結果になります。
>>> asyncio.run(main())
I say,
hello
一方で、awaitしなかった場合の挙動は少し異なります。
async def hello():
print('I say,')
await asyncio.sleep(1) # 1秒かかる
print('hello')
async def main():
asyncio.create_task(hello())
>>> asyncio.run(main())
I say,
asyncio.create_taskでは、タスクの実行も行われます。
そのため、print('I say,')は実行されていますがmainはhelloの完了を待たないためprint('hello')は実行されません。
複数のコルーチンを制御する
ここまででコルーチンとタスクを紹介しましたが、違いはどこに現れるのでしょうか。
async def hello():
print('I say,')
await asyncio.sleep(1) # 1秒かかる
print('hello')
async def goodbye():
print('you say,')
await asyncio.sleep(2) # 2秒かかる
print('goodbye')
async def main():
await goodbye()
await hello()
これを実行すると3秒かかり以下のようになります。
>>> asyncio.run(main())
you say,
goodbye
I say,
hello
goodbyeの完了を待ってからhelloの実行をしているためです。
asyncio.create_taskを使った場合
async def hello():
print('I say,')
await asyncio.sleep(1) # 1秒かかる
print('hello')
async def goodbye():
print('you say,')
await asyncio.sleep(2) # 2秒かかる
print('goodbye')
async def main():
goodbye_task = asyncio.create_task(goodbye())
hello_task = asyncio.create_task(hello())
await goodbye_task
await hello_task
これを実行すると2秒かかり以下のようになります。
>>> asyncio.run(main())
you say,
I say,
hello
goodbye
helloとgoodbyeはともに実行されており、実行された順にprint('you say,')とprint('I say,')が実行されます。
print('goodbye')は2秒後に実行されるので、先にprint('hello')が実行されます。
asyncio.gatherを使うことで、複数のコルーチンを並行に実行することができます。
コルーチンもタスクも使用することができます。
async def hello():
print('I say,')
await asyncio.sleep(1) # 1秒かかる
print('hello')
async def goodbye():
print('you say,')
await asyncio.sleep(2) # 2秒かかる
print('goodbye')
async def main():
await asyncio.gather(goodbye(), hello(), hello()) # タスクも同様
これを実行すると2秒かかり以下のようになります。
>>> asyncio.run(main())
you say,
I say,
I say,
hello
hello
goodbye
参考
Discussion