【メモ】非同期処理の基本を整理

# 概略
以下はこの記事の要点のみを記載しました。
- 非同期処理の基本: 非同期処理は、ある処理の完了を待たずに次の処理に進むことで、処理待ち時間を有効活用する。対照的に、同期処理はある処理が完了するまで次の処理に進まない
- 非同期処理のメリット: 時間のかかる処理とそうでない処理を分離し、並行して実行することで、UIのフリーズやレスポンス遅延を回避し、ユーザ体験を向上させる
- コルーチンの概念: 非同期処理はコルーチンを用いて実現される。コルーチンは、処理の途中で中断と再開が可能な処理の単位であり、サブルーチン（中断できない処理の塊）とは異なる
- asyncとawait: Pythonでは、async defでコルーチン関数を定義し、await式を使ってコルーチン内での処理の中断と再開を制御する。awaitは、指定された処理が完了するまでコルーチンの実行を一時停止し、その間CPUを他のタスクに割り当てる。処理完了後、結果を受け取って実行を再開する

## 実行ファイル
記事と同じ内容を含むnotebookをgithubにアップしました。実際に手を動かしながら実行される際に利用してみてください。
github: https://github.com/Tsucreator/learn_asynchronous

## 前置き
まだまだ駆け出しの初学者です。記載内容に誤りや読者へ誤解を招く記載がありましたら、後学のためご指摘いただけるととても励みとなります。有識者の方よろしくお願いします。

# 非同期処理とは
非同期処理とは、とある処理が終わらずとも次の処理を進めてしまう処理のことです。一方で同期処理とは、とある処理が終わってから次の処理を行うことです。

### 非同期処理のメリット
一言で言えば、処理待ち時間の有効活用、とまとめることができます。<br>例えば以下の例を見てみます。非同期処理について知るために、通常の同期処理と非同期処理を比較してみます。
2秒待つ処理: task1、1秒待つ処理: task2、を用意し、それぞれを続けて実行します。
全ての処理が完了するまでに必要な時間は3秒となります。

```python
import time

## 2秒待つ処理
def task1():
  print("タスク1開始")
  time.sleep(2)  # 2秒待機
  print("タスク1完了")

## 1秒待つ処理
def task2():
  print("タスク2開始")
  time.sleep(1)  # 1秒待機
  print("タスク2完了")

# これらを組み合わせて実行するとtask2が完了するまで3秒必要となる
task1()
task2()

# 結果
>>>タスク1開始
>>>タスク1完了
>>>タスク2開始
>>>タスク2完了
```
では、次に非同期処理を用いてtask1, 2を完了させてみます。（文法は追って説明）<br>2秒待つ処理: task1、1秒待つ処理: task2、がありましたので、task1を実行している間に、task2も実行するように実装をします（このように同時に複数の処理を進めることを並列処理といいます）。<br>task1の2秒間の処理を待つあいだに、task2が完了するため、合計のtask完了時間は2秒に削減できます。

```python
import asyncio

## 2秒待つ処理
async def task1():
  print("タスク1開始")
  await asyncio.sleep(2)  # 2秒待機
  print("タスク1完了")

## 1秒待つ処理
async def task2():
  print("タスク2開始")
  await asyncio.sleep(1)  # 1秒待機
  print("タスク2完了")

# 並列処理を用いてtask1, 2を2秒で終える
await asyncio.gather(task1(), task2())

# 結果
>>>タスク1開始
>>>タスク2開始
>>>タスク2完了
>>>タスク1完了
```
このように非同期処理を取り扱うことができれば、時間のかかる処理とそうでない処理を分離して取り扱うことが可能となります。<br>処理速度の使い分けは、UIのフリーズやレスポンス遅延を回避することがユーザ体験の向上などのメリットがあります。

# 非同期処理の仕組み
### サブルーチンとコルーチン
非同期処理について説明がされる際には"コルーチン(coroutine)"という表現が出てきます。<br>サブルーチンとコルーチンはそれぞれが以下のイメージとなります。
- サブルーチン: 途中の出口がないトンネル。途中で中断ができない一連の処理のかたまり
- コルーチン: 途中に非常口がたくさん付いているトンネル。途中で中断と再開ができる処理のかたまり

非同期処理では"コルーチン"という処理（関数）を定義し、コルーチンの中で中断・再開を操作することで非同期処理を実現します。

### コルーチン関数定義
pythonで非同期処理を実現する場合、`async def`として関数を定義します。定義された関数はコルーチン関数となるため、非同期の処理を含むことや、非同期処理の対象として扱うことが可能です。

## 非同期処理の実行（`await`式の効果）
繰り返しになりますが、非同期として実行したい処理というのは多くの場合が**時間のかかる処理**です。一連の処理（コルーチン）の中に、時間のかかる処理が含まれているとき、その時間のかかる処理が実行されているあいだはそのコルーチンを中断し、別の処理の実行を優先する、これが非同期処理の使い方となります。<br>この点は、コルーチンの中に**時間のかかる処理**があれば、その処理が終わるまでコルーチン内の次の処理を実行しないように**「待っている」必要がある**と解釈できます。つまり、時間がかかる処理に対して`await（ここで待つ）`と指示をすることで、`await`式が完了するまでコルーチン内の処理を中断します。

`await`式の効果をまとめると以下の通りです。
- 処理の一時停止: `await`に続くコルーチン（awaitableオブジェクト）の実行が完了するまで、現在のコルーチンの実行を一時停止
- CPU の解放: `await`によって一時停止したコルーチンは、ファイルI/Oなどの時間がかかる処理を行っていることが多い。その間、CPU は他のタスクに割り当てられる
- 処理の再開: `await` に続く処理の実行が完了すると、`await` で一時停止していたコルーチンは、その結果を受け取って実行を再開する
- 結果の受け取り: `await` 式を利用すると、中断していた処理の結果を受け取ることができる。これにより、非同期処理の結果を同期的なコードのように扱うことが可能となる

### 実装
イメージを掴むため`await`式を用いて非同期処理を実装してみます。唐突に`asyncio.create_task()`というメソッドを記載しておりますが、ここでは非同期関数の実行開始と理解してください。

```python
import asyncio

## 時間がかかる処理の例（例えばI/O処理など）
async def long_running_task(task_id):
    print(f"task {task_id}: 開始")
    await asyncio.sleep(3)  # 3秒待機
    print(f"task {task_id}: 完了")
    return f"task {task_id} の結果"

## メインの処理
async def main():
    print("long_running_taskを開始しました。")
    task1 = asyncio.create_task(long_running_task(1)) # long_running_taskその1の実行開始
    task2 = asyncio.create_task(long_running_task(2)) # long_running_taskその2の実行開始

    await asyncio.sleep(1) # 表示順序の調整（無視してok）

    ## 以下は模擬的にtask1, 2を中断している間の処理を表現したもの。task1, 2が裏で動いているうちに先に実行する処理。
    print("task1, 2は時間がかかるため他の処理を実行します...")
    await asyncio.sleep(1) # 他の処理のシミュレーション(1秒で終わる)
    print("他の処理が完了しました。")

    ## 先述のawait式のもつ効果のうち"結果の受け取り"と記載したもの。実行完了を待ちlong_running_taskの結果を受け取る。
    result1 = await task1  # task1の結果を受け取る
    result2 = await task2  # task2の結果を受け取る

await main()

# 結果
>>>long_running_taskを開始しました。
>>>task 1: 開始
>>>task 2: 開始
>>>task1, 2は時間がかかるため他の処理を実行します...
>>>他の処理が完了しました。
>>>task 1: 完了
>>>task 2: 完了
```

基本的な非同期処理の考え方・実行については以上です。まとめると以下の通りです。

- 非同期処理の基本: 非同期処理は、ある処理の完了を待たずに次の処理に進むことで、処理待ち時間を有効活用する。対照的に、同期処理はある処理が完了するまで次の処理に進まない
- 非同期処理のメリット: 時間のかかる処理とそうでない処理を分離し、並行して実行することで、UIのフリーズやレスポンス遅延を回避し、ユーザ体験を向上させる
- コルーチンの概念: 非同期処理はコルーチンを用いて実現される。コルーチンは、処理の途中で中断と再開が可能な処理の単位であり、サブルーチン（中断できない処理の塊）とは異なる
- asyncとawait: Pythonでは、async defでコルーチン関数を定義し、await式を使ってコルーチン内での処理の中断と再開を制御する。awaitは、指定された処理が完了するまでコルーチンの実行を一時停止し、その間CPUを他のタスクに割り当てる。処理完了後、結果を受け取って実行を再開する