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