asyncioに入門する

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

# asyncio — Asynchronous I/O

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

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

```py:Hello World!
import asyncio

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

asyncio.run(main())
```

以下のように出力されます。1行目と2行目の出力の間には1秒空いていました。

```sh
Hello ...
... World!
```

短いサンプルコードですが、（asyncioを全く使ったことがない人にとっては）見慣れない記述がいくつもあります。

- ①`def main():`の前に付いている`async`とは何か
- ②`await`とは何か
- ③`asyncio.sleep()`は`time.sleep()`と何が違うか
- ④`asyncio.run()`とは何か

このままドキュメントを読み進めて行けばわかることだとは思いますが、まずはそれぞれを見慣れた記述に変更して実行することで挙動を確認してみます。


## ①`def main():`の前に付いている`async`とは何か

`async`を外してみます。

```py:1-1
import asyncio

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

asyncio.run(main())
```

```sh
SyntaxError: 'await' outside async function
```

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

## ②`await`とは何か

`await`を外してみます

```python:2-1
import asyncio

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

asyncio.run(main())
```

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

```python:3-1
import asyncio
import time

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

asyncio.run(main())
```

```sh
Hello ...
Traceback (most recent call last):
...
TypeError: object NoneType can't be used in 'await' expression
```

`time.sleep()`が何も返却しないので、そこで`await`を使ってしまうことに問題がありそうです。

ちなみに以下のように`time.sleep()`に置換した上で`await`も削除すると、サンプルコード`Hello World!`と同じように振る舞います。

```python:3-2
import asyncio
import time

async def main():
    print('Hello ...')
    time.sleep(1)
    print('... World!')

asyncio.run(main())
```

```sh
Hello ...
... World!
```

こうなると、以下の２つのコードの違いも気になります。

```python
time.sleep()
```

```python
await asyncio.sleep()
```

## ④`asyncio.run()`とは何か
`asyncio.run()`を使わずに直接`main()`を呼んでみます。

```python:4-1
import asyncio

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

main()
```

```sh
RuntimeWarning: coroutine 'main' was never awaited main()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
```

`await`を外したときの警告に似ていますね。さらに`main`の`async`を外してみるとどうなるでしょうか？

```python:4-2
import asyncio

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

main()
```

```sh
SyntaxError: 'await' outside async function
```

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


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

# Coroutines and Tasks

## Coroutines¶

```py
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を確認してみましょう。

```python
import asyncio

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

asyncio.run(main())
```

```sh
...
<class 'coroutine'>
...
```

`awaitable object`のようなものが返ってきているかと思いきやコルーチンオブジェクトが返却されているようです。これはどういうことでしょうか。

ドキュメントによると`asincio.sleep()`はたしかにコルーチン関数なので、やはりコルーチンオブジェクトが返却されるのは期待通りです。

https://docs.python.org/3/library/asyncio-task.html#sleeping

ドキュメントにきちんと記載がありました。

*awaitableオブジェクトには、コルーチン、タスク、フューチャーの3つの主なタイプがあります。*

つまり、コルーチンオブジェクトはawaitableオブジェクトの１つのようです。

これで、また冒頭の疑問の一部が解消しました。


### ③`asyncio.sleep()`は`time.sleep()`と何が違うか

`asyncio.sleep()`はコルーチン関数であり、コルーチンオブジェクトを返却するので、await式とともに使うことができる。ということです。また、この疑問は「②`await`とは何か」と同じことを指していたということです。

ただし、以下の2つのコードの違いについては疑問のままです。

```python
time.sleep()
```

```python
await asyncio.sleep()
```

`await`式は何のために用いるのでしょうか。公式ドキュメントに記載があります

*awaitableオブジェクトでのコルーチンの実行を中断します。コルーチン関数内でのみ使用できます。*

ここでまた新たに疑問が浮かびます。

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

さらにドキュメントを読み進めていくと、これらの疑問が解消していきます。

## Tasks

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

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

```python: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())
```

```sh
hello
hello
world
world
```

うまくいきました。なぜ並行処理ができたのでしょうか？ドキュメントに以下のような記述があります。

*sleep()は常に現在のタスクを中断し、他のタスクを実行できるようにします。*

つまり、`task1`の中で`asyncio.sleep(1)`が実行されたタイミングでタスクが中断され`task2`に実行が移ったということになります。

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

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

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

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


```python: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())
```

```sh
hello
world
hello
world
```


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

```python: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())
```

```sh
hello
hello
world
world
```

`gather`を用いる場合は`create_task`を省略できます。なぜなら以下のような仕様であるためです。

*awaitable がコルーチンである場合、自動的にタスクとしてスケジュールされます。*

```python: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())

```

```sh
hello
hello
world
world
```