pythonのasyncioを非同期処理知識0から勉強してみた

この記事は非同期処理の知識が0の私が
[こちら](https://qiita.com/everylittle/items/57da997d9e0507050085)の記事やその他の情報をまとめたものになります。

私自身細かいコードを書くわけではないので、この記事ではざっくりとした理解を目的とし、他の記事をスラスラ読めるようになることができればいいというポジションです。

# asyincio とは
並列処理には「マルチスレッド」、「マルチプロセス」、「ノンブロッキング」の３種類ありますが、asyncioは「ノンブロッキング」に相当します。
詳細な説明は[こちら](https://qiita.com/icoxfog417/items/07cbf5110ca82629aca0)
に委ねます。

「ノンブロッキング」とは、1スレッドで複数のリクエストを処理するもので、スレッドが増えてメモリ不足にならないというメリットがあります。

デメリットや使い所、書き方の基本はこの記事を読むことでわかるようになります。

# asyncioのサンプルコード

まずは次の例を見てみましょう。`async`, `await`などが見慣れないですが、二つの非同期関数があり、それぞれにスリープの処理が入っているだけです。

```python
import asyncio

async def func1():
    print('func1() started')
    await asyncio.sleep(1)
    print('func1() finished')

async def func2():
    print('func2() started')
    await asyncio.sleep(1)
    print('func2() finished')

async def main():
    task1 = asyncio.create_task(func1())
    task2 = asyncio.create_task(func2())
    await task1
    await task2

asyncio.run(main()) # こちらpython3.7以降の書き方らしいです
```
```
func1() started
func2() started
func1() finished
func2() finished
```


func1の処理の最中にfunc2の処理が割り込まれていますね。
なぜこの挙動になるのか少しずつ紐解いていきましょう。

知らなければならない前提知識が多数あるので、遠回りですが頑張りましょう。

# 前提知識１：コルーチン(co-routine)
まずはコルーチンというものを知ってもらいます。

サブルーチンがエントリーからリターンまでを一つの処理単位とするのに対し、コルーチンはいったん処理を中断した後、続きから処理を再開できます。
接頭辞 co は協調を意味します。「他の処理と協調します」ということでしょうね。

pythonにおける最小のコルーチンは次のようになります。

```python
async def f(n):
  return n
```
`async`を関数定義の先頭に置くことで、「これがコルーチンだ！」というのをpyhonに教えることになります。

しっかりと、`async`を書いていない関数内で、非同期処理をしようとするとエラーになります。

例えば次のコードはエラーになります。

```python
import asyncio

def f():
    await asyncio.sleep(1)

asyncio.run(f())
```

エラー文として`'await' outside async function`と言われてしまいます。
**asyncとawaitはセットだということを覚えておきましょう。**

また、コルーチン関数は普通の関数と挙動が異なります。

```python
import asyncio

async def f():
    return 100

print(f)
```

```
<function f at 0x0000025479C403A0>
```

普通の関数であれば、`100`が返ってきますが、この例では**コルーチンオブジェクト**という謎のオブジェクトが返ってきます。
このコルーチンオブジェクトを実行するには、**asyncioのイベントループ**に放り込む必要があります。

# 前提知識２：イベントループ
調べるとイベントループはかなり複雑なことをしているようで、理解しきれなかったのですが、どうやら初学者レベルでは「複数のコルーチンをいい感じに管理・処理してくれる、複雑な処理のループ」くらいの理解で十分みたいです。

ただ、イベントループの機能をすべて理解しなくとも、１つくらいは知っておきましょう。

```python
import asyncio

async def f():
    print("fが実行されたよ")
    return 100

coro = f()  # コルーチンオブジェクトを作成
loop = asyncio.get_event_loop() # イベントループを作成
print(loop)
task = loop.create_task(coro) # イベントループにコルーチンを登録
print(task)
```
```
<ProactorEventLoop running=False closed=False debug=False>
<Task pending name='Task-1' coro=<f() running at c:/Users/username/Desktop/program.py:3>>
```
このように、コルーチンオブジェクトが登録され、それらを管理するというのがイベントループの機能の一端です。だいたいイメージできたかと思います。(そういうことにします笑)
（補足：コルーチンオブジェクトはコルーチン関数を呼び出すと返ってくるオブジェクトであり、コルーチン関数ではないです。）

`print(task)`で`<Task pending name='Task-1'...`と出力されていますが、こちらはタスクオブジェクトといい、コルーチンオブジェクトの実行状態を管理するオブジェクトです。

一旦、「ふ～ん」くらいのままでOKです。タスクオブジェクトについては、後でまたでてきます。

この`task`を実際に走らせるには`loop.run_until_complete()`というメソッドを使います。

```python
import asyncio

async def f():
    print("fが実行されたよ")
    return 100

coro = f() # コルーチンオブジェクトを作成
loop = asyncio.get_event_loop() # イベントループを作成
task = loop.create_task(coro) # イベントループにコルーチンを登録
loop.run_until_complete(task) # タスクが終了するまでイベントループを実行
# result = loop.run_until_complete(task) # このようにすればresultに100が格納される

```
```
fが実行されたよ
```
こんな、長ったらしいのを書いてようやく実行されましたね。
はっきり言ってこんなに書くのめんどくさいですよね。ということで、python3.7から使えるようになった`asyncio.run()`が便利です。
次のように短くなります。

```python
coro = coro_func() # コルーチンオブジェクトを作成
asyncio.run(coro) # イベントループを作成し、コルーチンオブジェクトが終了するまで実行
```
これなら書く気になりますね。

`asyncio.run()`は次の処理を行うようです。
1. あたらしくイベントループを作成します。
2. 指定されたコルーチンオブジェクトをイベントループに登録し、タスクオブジェクトを作成します。
3. タスクオブジェクトが完了するまでイベントループを実行し続けます。

便利ですね。これでイベントループのざっくりした理解を終えます。
最後にもう一度いうと、イベントループは「複数のコルーチンをいい感じに管理・処理してくれる、複雑な処理のループ」です。

# 前提知識３：Future
`Future`は処理の結果を格納するためのオブジェクトです。
JavaScriptでいう`Promise`に相当するらしいです。

`Future`には「結果」と「ステータス」という２つの情報を持っています。

次のコードを見てください。

```python
>>> future = loop.create_future()
>>> future.set_result(100)

>>> future
<Future finished result=100>
```
`future `が`finished`というステータスと`100`という結果を持っている事がわかりますね。

ステータスには「pending」「finished」「cancelled」の３種類があり、 初期状態は「pending」で、終了状態は「finished」か「cancelled」のいずれかです。

なお、通常ではこの例のようにわざと`Future`オブジェクトが作られることはありません。このオブジェクトはただ単に「状態と結果を保存するための箱」です。この「箱」をうまく使ってイベントループは処理の状態を把握します。


# 前提知識４：Future/Coroutine/Taskの違い
個人的に気になったので、これまでのまとめも兼ねて、ここで整理しておきます。
[公式ドキュメント1](https://docs.python.org/ja/3/library/asyncio-task.html),[公式ドキュメント2](https://docs.python.org/ja/3.6/library/asyncio-task.html#task)を見るとざっくり理解できます。


- `タスクは、イベントループ内のコルーチンオブジェクトの実行を担当する。`
- `タスクはFutureのサブクラスである。`  

と公式ドキュメントに書いてありました。
あくまで私の理解ですが、`Task`は`Future`という処理の状態を記述できる用紙を片手に、実行命令を`Coroutine`に出したり、処理結果や処理状況を`Future`を通して`Coroutine`から受け取り、上司であるイベントループに`Future`を通して報告するのが役割だと思います（かなり不正確ですがこの記事ではイメージがつかめればOKです）。

相関図は次の通りです。これだけの情報だと「タスクいらない子」になるので、実際の`Task`はもっと細かいことをやっているのだと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/816697/99338389-fe13-215b-cac0-13bc1c71350f.png)


# await
やっとずっとモヤモヤしていた`await`について解説できますね。

`await`にはルールがあります。
`await`文に指定できるオブジェクトは、 `Coroutine`と`Future`、`Task`の３つです（これらを`Awaitable`オブジェクトと言います）。

ただ、`Future`を直接使うことは無いので

```python
await taskオブジェクト
await Coroutineオブジェクト
```

のようにして使うみたいです。
「**`Coroutiine`や`Task`を実行させる行では、書類`Future`のやり取りが必要になるため、そのような行では`wait`を先頭につけるといる決まりになっている**」といってしまったほうがわかりやすいかもしれませんね。

# asyncio.sleep(1)とtime.sleep(1)の違いからわかる全体像

ここで衝撃の事実をお伝えするのですが、`asyncio`は一つのタスクしか実行できません。

「あれ？並列処理じゃないの？」と思いますが、`asyncio`は「CPUを使わない待ち」が生じてる間に次に優先順位の高いタスクにCPUのリソースを割り当てることしかできません。
（決して、CPUのリソースを要する処理を２つ同時に実行できるわけではありません）

結局のところ、asyncio.sleep(1)とtime.sleep(1)の違いは「この処理はCPUを使わない待ちだよー」というのを、イベントループに伝える能力の有無にあります。

ここで、冒頭のサンプルコードとの違いに注意しながら次のコードを見てください。

```python
import asyncio
import time

async def func1():
    print('func1() started')
    await asyncio.sleep(1) # 上に「CPUを使わない待ち」ということを伝え,CPUの制御をイベントループに渡す
    print('func1() finished')

async def func2():
    print('func2() started')
    time.sleep(2) # 上に「CPUを使わない待ち」ということを伝えられず終わるまで待つしか無い
    print('func2() finished')

async def main():
    task1 = asyncio.create_task(func1())
    task2 = asyncio.create_task(func2())
    await task1 # タスクもawaitがないと、イベントループに「CPUを使わない待ち」ということを伝えられない
    await task2

asyncio.run(main())
```
```
func1() started
func2() started
func2() finished
func1() finished
```
冒頭のサンプルコードとは異なり、`func1()`と`func2()`の終了タイミングが逆転していますね。これは、`func2()`の「待ち」の間に`func1()`に制御が戻らなかったことを意味しています。
「func2() started」が表示された直後までの流れのざっくりとしたイメージ図は次のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/816697/d9b49f68-5b17-79b3-04ea-432f56537bde.png)


`time.sleep()`はコルーチンではなくサブルーチンなので、イベントループに「CPUを使わない待ち」ということを伝えられず、素直に終わるまで待つしか無いです。
また、別の捉え方をすると、`asyncio.sleep()`で他人に順番を譲ることはできても、自発的に割り込むことはできないということになります。

なお、「CPUを使わない待ち」が実際に生じる場面は、「webへの問い合わせを行った際に、結果が帰ってくるまで待つ」などが挙げられます。確かに、web関連のライブラリでよく`asyncio`を見ますね。

以上、ざっくりとした説明でしたが、ご指摘等あればコメントいただけると幸いです。
勉強しながら記事を書いたので正確ではない部分もありますが、用語を解説しながら説明したので初心者に優しい文章にはなっているはずです。（しかし文章力。。）
ここまで理解すれば、他の記事の情報もスムーズに理解できると思います。（私自身が）