LoginSignup
4
8

More than 1 year has passed since last update.

asyncioに入門する

Posted at

公式ドキュメントの説明やサンプルコードをベースに、asyncioに入門します。

asyncio — Asynchronous I/O

asyncio は、 async/await構文を使用して並行コードを作成するためのライブラリです。

まずは公式のサンプルコードを動かしてみましょう。

Hello World!
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を外してみます。

1-1
import asyncio

def main():
    print('Hello ...')
    await asyncio.sleep(1)
    print('... World!')

asyncio.run(main())
SyntaxError: 'await' outside async function

どうやらasyncを付けていない関数の中ではawaitを使ってはいけないようです。

awaitとは何か

awaitを外してみます

2-1
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()に置換してみます。

3-1
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!と同じように振る舞います。

3-2
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()を呼んでみます。

4-1
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を外したときの警告に似ていますね。さらにmainasyncを外してみるとどうなるでしょうか?

4-2
import asyncio

def main():
    print('Hello ...')
    await asyncio.sleep(1)
    print('... World!')

main()
SyntaxError: 'await' outside async function

よくよく見るとmain()の宣言時点でSyntaxErrorになっています。サンプルコード1-1main()は同じですから、同じ結果になって然るべきでした。

これらの疑問が解決することを期待して、ドキュメントを読み進めます。

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

タスクは、コルーチンを同時にスケジュールするために使用されます。

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()を使ってタスクを作り並行処理してみます。

concurrency_ok
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に実行が移ったということになります。

これで先程新たに浮かんだ疑問の答えが出ました。

⑤コルーチン実行を中断するということはどういうことで、どんなメリットがあるのでしょうか?

複数のタスクが存在する場合に他のタスクに実行を移すことで並行処理が可能になる。

タスクを作らずに以下のような記述をしても、これは並行処理されません。

concurrency_ng
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

このような書き方もできます。

concurrency_gather
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 がコルーチンである場合、自動的にタスクとしてスケジュールされます。

concurrency_gather_without_task
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
4
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
8