公式ドキュメントの説明やサンプルコードをベースに、asyncioに入門します。
asyncio — Asynchronous I/O
asyncio は、 async/await構文を使用して並行コードを作成するためのライブラリです。
まずは公式のサンプルコードを動かしてみましょう。
import asyncio
async def main():
print('Hello ...')
await asyncio.sleep(1)
print('... World!')
asyncio.run(main())
以下のように出力されます。1行目と2行目の出力の間には1秒空いていました。
Hello ...
... World!
短いサンプルコードですが、(asyncioを全く使ったことがない人にとっては)見慣れない記述がいくつもあります。
- ①
def main():
の前に付いているasync
とは何か - ②
await
とは何か - ③
asyncio.sleep()
はtime.sleep()
と何が違うか - ④
asyncio.run()
とは何か
このままドキュメントを読み進めて行けばわかることだとは思いますが、まずはそれぞれを見慣れた記述に変更して実行することで挙動を確認してみます。
①def main():
の前に付いているasync
とは何か
async
を外してみます。
import asyncio
def main():
print('Hello ...')
await asyncio.sleep(1)
print('... World!')
asyncio.run(main())
SyntaxError: 'await' outside async function
どうやらasync
を付けていない関数の中ではawait
を使ってはいけないようです。
②await
とは何か
await
を外してみます
import asyncio
async def main():
print('Hello ...')
asyncio.sleep(1)
print('... World!')
asyncio.run(main())
Hello ...
RuntimeWarning: coroutine 'sleep' was never awaited asyncio.sleep(1)
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
... World!
実行自体はできたようですが、警告が複数出ているようです。どうやらasyncio.sleep()
はコルーチン
というものを返しており、await
をつけることに意味があったようです。また1秒のsleepが発生しませんでした。
③asyncio.sleep()
はtime.sleep()
と何が違うか
asyncio.sleep()
をtime.sleep()
に置換してみます。
import asyncio
import time
async def main():
print('Hello ...')
await time.sleep(1)
print('... World!')
asyncio.run(main())
Hello ...
Traceback (most recent call last):
...
TypeError: object NoneType can't be used in 'await' expression
time.sleep()
が何も返却しないので、そこでawait
を使ってしまうことに問題がありそうです。
ちなみに以下のようにtime.sleep()
に置換した上でawait
も削除すると、サンプルコードHello World!
と同じように振る舞います。
import asyncio
import time
async def main():
print('Hello ...')
time.sleep(1)
print('... World!')
asyncio.run(main())
Hello ...
... World!
こうなると、以下の2つのコードの違いも気になります。
time.sleep()
await asyncio.sleep()
④asyncio.run()
とは何か
asyncio.run()
を使わずに直接main()
を呼んでみます。
import asyncio
async def main():
print('Hello ...')
await asyncio.sleep(1)
print('... World!')
main()
RuntimeWarning: coroutine 'main' was never awaited main()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
await
を外したときの警告に似ていますね。さらにmain
のasync
を外してみるとどうなるでしょうか?
import asyncio
def main():
print('Hello ...')
await asyncio.sleep(1)
print('... World!')
main()
SyntaxError: 'await' outside async function
よくよく見るとmain()
の宣言時点でSyntaxErrorになっています。サンプルコード1-1
とmain()
は同じですから、同じ結果になって然るべきでした。
これらの疑問が解決することを期待して、ドキュメントを読み進めます。
Coroutines and Tasks
Coroutines¶
import asyncio
async def main():
print('Hello ...')
await asyncio.sleep(1)
print('... World!')
run(main())
コルーチンを呼び出すだけでは、実行されるようにスケジュールされないことに注意してください。
コルーチンという用語が唐突に何の説明もないまま登場しました。ただ、文脈からしてasync
を付与して宣言している関数が、コルーチンと呼ばれていると推測できます。Pythonの公式ドキュメントのリンクがあったので、そちらも参照してみます。
コルーチンは、サブルーチンをより一般化した形式です。サブルーチンはある時点で開始され、別の時点で終了します。コルーチンは、さまざまなポイントで開始、終了、再開できます。それらはステートメントで実装できます
なるほど。推測はあたっていたいようです。確かに様々なポイントで開始・終了・再開ができるのであれば便利そうです。
Python公式ドキュメントのコルーチンの下にコルーチン関数の記述があります。
コルーチンオブジェクトを返す関数
コルーチン関数は関数の一種でコルーチンオブジェクトを返すということですが、単にコルーチンといった場合はコルーチン関数とコルーチンオブジェクトのどちらを指しているのか文脈判断が必要そうです。
これで冒頭の疑問のいくつが解消しました。
①def main():
の前に付いているasync
とは何か
コルーチン関数を宣言するために付与しているのです。
④asyncio.run()
とは何か
ここまでの内容で、最初のサンプルコードでmain()
の実行のためにasyncio.run()
がなぜ必要だったかがわかります。コルーチン関数は(そうでない)関数と比較してさまざまなポイントで開始できるので、裏を返すと開始するポイントを示す必要があります。(単に呼び出すだけではコルーチンオブジェクトが返るだけということ。)その開始を示すために用いるのがasyncio.run()
ということです。
Awaitables
オブジェクトがawait式で使用できる場合、そのオブジェクトはawaitableオブジェクトであると言います。
つまり冒頭のサンプルコードに戻るとasyncio.sleep()
はawaitableオブジェクトを返却しているようです。
実際にコードを動かしてtypeを確認してみましょう。
import asyncio
async def main():
print('Hello ...')
print(type(asyncio.sleep(1)))
print('... World!')
asyncio.run(main())
...
<class 'coroutine'>
...
awaitable object
のようなものが返ってきているかと思いきやコルーチンオブジェクトが返却されているようです。これはどういうことでしょうか。
ドキュメントによるとasincio.sleep()
はたしかにコルーチン関数なので、やはりコルーチンオブジェクトが返却されるのは期待通りです。
ドキュメントにきちんと記載がありました。
awaitableオブジェクトには、コルーチン、タスク、フューチャーの3つの主なタイプがあります。
つまり、コルーチンオブジェクトはawaitableオブジェクトの1つのようです。
これで、また冒頭の疑問の一部が解消しました。
③asyncio.sleep()
はtime.sleep()
と何が違うか
asyncio.sleep()
はコルーチン関数であり、コルーチンオブジェクトを返却するので、await式とともに使うことができる。ということです。また、この疑問は「②await
とは何か」と同じことを指していたということです。
ただし、以下の2つのコードの違いについては疑問のままです。
time.sleep()
await asyncio.sleep()
await
式は何のために用いるのでしょうか。公式ドキュメントに記載があります
awaitableオブジェクトでのコルーチンの実行を中断します。コルーチン関数内でのみ使用できます。
ここでまた新たに疑問が浮かびます。
- ⑤コルーチン実行を中断するということはどういうことで、どんなメリットがあるのでしょうか?
さらにドキュメントを読み進めていくと、これらの疑問が解消していきます。
Tasks
タスクは、コルーチンを同時にスケジュールするために使用されます。
import asyncio
async def nested():
return 42
async def main():
# Schedule nested() to run soon concurrently
# with "main()".
task = asyncio.create_task(nested())
# "task" can now be used to cancel "nested()", or
# can simply be awaited to wait until it is complete:
await task
asyncio.run(main())
先程の記述にタスクもawaitableオブジェクトであるとありました。
awaitableオブジェクトには、コルーチン、タスク、フューチャーの3つの主なタイプがあります。
ではcreate_task()
を使ってタスクを作り並行処理してみます。
import asyncio
async def sleep():
print('hello')
await asyncio.sleep(1)
print('world')
async def main():
task1 = asyncio.create_task(sleep())
task2 = asyncio.create_task(sleep())
await task1
await task2
asyncio.run(main())
hello
hello
world
world
うまくいきました。なぜ並行処理ができたのでしょうか?ドキュメントに以下のような記述があります。
sleep()は常に現在のタスクを中断し、他のタスクを実行できるようにします。
つまり、task1
の中でasyncio.sleep(1)
が実行されたタイミングでタスクが中断されtask2
に実行が移ったということになります。
これで先程新たに浮かんだ疑問の答えが出ました。
⑤コルーチン実行を中断するということはどういうことで、どんなメリットがあるのでしょうか?
複数のタスクが存在する場合に他のタスクに実行を移すことで並行処理が可能になる。
タスクを作らずに以下のような記述をしても、これは並行処理されません。
import asyncio
async def sleep():
print('hello')
await asyncio.sleep(1)
print('world')
async def main():
await sleep()
await sleep()
asyncio.run(main())
hello
world
hello
world
このような書き方もできます。
import asyncio
async def sleep():
print('hello')
await asyncio.sleep(1)
print('world')
async def main():
task1 = asyncio.create_task(sleep())
task2 = asyncio.create_task(sleep())
await asyncio.gather(task1, task2)
asyncio.run(main())
hello
hello
world
world
gather
を用いる場合はcreate_task
を省略できます。なぜなら以下のような仕様であるためです。
awaitable がコルーチンである場合、自動的にタスクとしてスケジュールされます。
import asyncio
async def sleep():
print('hello')
await asyncio.sleep(1)
print('world')
async def main():
await asyncio.gather(sleep(), sleep())
asyncio.run(main())
hello
hello
world
world