Pythonの非同期処理(asyncio)の使い方を理解する

# 概要
最近流行りに流行っているLLMを利用するにあたり、ストリーミングでのレスポンス表示や複数のリクエストによる遅延の防止を目的とし、非同期処理を利用するケースが増えている気がします。ということで、asyncioの挙動を今一度整理してみました。
本記事では、以下の4ケース+αの書き方を記しています

1. 同期関数から同期関数を呼ぶ
1. 同期関数から非同期関数を呼ぶ
1. 非同期関数から同期関数を呼ぶ
1. 非同期関数から非同期関数を呼ぶ

# 同期関数から同期関数を呼ぶ

非同期処理を何も考えない普通のケースです。

```python
import time


def sync_func():
    print("func: start sync_func")
    time.sleep(1)
    print("func: end sync_func")

def main():
    print("main: start main")
    print("main: before func")
    sync_func()
    print("main: after func")
    time.sleep(3)
    print("main: end main")


if __name__ == '__main__':
    main()
```

結果は当然以下のように、sync_funcの完了を待った上で、`print("main: after func")`が実行されています

```
main: start main
main: before func
func: start sync_func
func: end sync_func
main: after func
main: end main
```

# 同期関数から非同期関数を呼ぶ

`sync_func`を`async_func`と非同期化してあげます

```python
import asyncio
import time


async def async_func():
    print("func: start async_func")
    await asyncio.sleep(1)
    print("func: end async_func")

def main():
    print("main: start main")
    print("main: before func")
    asyncio.run(async_func())
    print("main: after func")
    time.sleep(3)
    print("main: end main")


if __name__ == '__main__':
    main()
```

結果は、、、

```
main: start main
main: before func
func: start async_func
func: end async_func
main: after func
main: end main
```

`asyncio.run`で同期関数から非同期関数を呼ぶことができ、同期関数から同期関数を呼ぶケースと同じく、async_funcの完了を待った上で、`print("main: after func")`が実行されます。
これは、`asyncio.run`が、イベントループを開始し、提供されたコルーチン (async_func) が完了するまで待ち、そしてイベントループを閉じるという一連の処理を行うためです。

# 非同期関数から同期関数を呼ぶ

今度は逆に、非同期関数から同期関数を呼んでみます

```python
import asyncio
import time


def sync_func():
    print("func: start sync_func")
    time.sleep(1)
    print("func: end sync_func")

async def main():
    print("main: start main")
    print("main: before func")
    sync_func()
    print("main: after func")
    await asyncio.sleep(3)
    print("main: end main")


if __name__ == '__main__':
    asyncio.run(main())
```

結果は、同期関数`sync_func`の完了を待ってから`print("main: after func")`が実行されています

```
main: start main
main: before func
func: start sync_func
func: end sync_func
main: after func
main: end main
```

## 同期関数の完了を待たない

では同期関数を待たないようにするにはどうすれば良いかというと、別スレッドなり（デフォルトでは、[ThreadPoolExecutor](https://docs.python.org/ja/3/library/concurrent.futures.html#threadpoolexecutor)が利用されるようです）で実行できる`run_in_executor()`を利用してあげれば良いです

```python
import asyncio
import time


def sync_func():
    print("func: start sync_func")
    time.sleep(1)
    print("func: end sync_func")

async def main():
    print("main: start main")
    print("main: before func")
    
    loop = asyncio.get_event_loop()
    loop.run_in_executor(None, sync_func)

    print("main: after func")
    await asyncio.sleep(3)
    print("main: end main")


if __name__ == '__main__':
    asyncio.run(main())
```

```
main: start main
main: before func
func: start sync_func
main: after func
func: end sync_func
main: end main
```

同期関数`sync_func`の完了を待たずに次の処理`print("main: after func")`が実行されています

# 非同期関数から非同期関数を呼ぶ

最後に非同期関数から非同期関数を呼ぶケースですが、これが一番複雑です。

```python
import asyncio
import time


async def async_func():
    print("func: start async_func")
    await asyncio.sleep(1)
    print("func: end async_func")

async def main():
    print("main: start main")
    print("main: before func")
    async_func()
    print("main: after func")
    await asyncio.sleep(3)
    print("main: end main")


if __name__ == '__main__':
    asyncio.run(main())
```

```
main: start main
main: before func
RuntimeWarning: coroutine 'async_func' was never awaited
  async_func()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
main: after func
main: end main
```

単に非同期関数`async_func()`を呼ぼうとしても、そもそも実行されません。`async def`で定義した関数はコルーチンを返すので、呼び出すだけでは実行されず、awaitをつけるとその場所で初めて実行されることになります。

## 非同期関数にawaitをつける

```python
import asyncio
import time


async def async_func():
    print("func: start async_func")
    await asyncio.sleep(1)
    print("func: end async_func")

async def main():
    print("main: start main")
    print("main: before func")
    res = async_func()
    print("main: after func")
    print("main: before await")
    await res
    print("main: after await")
    await asyncio.sleep(3)
    print("main: end main")


if __name__ == '__main__':
    asyncio.run(main())
```

```
main: start main
main: before func
main: after func
main: before await
func: start async_func
func: end async_func
main: after await
main: end main
```

`print("main: before await")`と`print("main: after await")`の間で実行の開始から完了が起こっていることがわかります。

## 非同期関数の呼び出し時点から実行を開始してほしい場合

`create_task()`関数を利用するとtaskをスケジュールすることができます。

```python
import asyncio
import time

async def async_func():
    print("func: start async_func")
    await asyncio.sleep(1)
    print("func: end async_func")

async def main():
    print("main: start main")
    print("main: before func")
    task = asyncio.create_task(async_func())
    print("main: after func")
    await asyncio.sleep(3)
    print("main: end main")


if __name__ == '__main__':
    asyncio.run(main())
```

```
main: start main
main: before func
main: after func
func: start async_func
func: end async_func
main: end main
```

あれ、`print("main: after func")`の後に`async_func`が実行されていますね。これではダメそうです、、、

## 明示的にイベントループに制御を戻す

上記の問題を解決するためには、現在のTaskを一時中断し、イベントループに制御を戻す（＝他のTaskが実行されるのを許可する）必要があります。
そのための魔法の呪文が`await asyncio.sleep(0)`です。

```python
import asyncio
import time

async def async_func():
    print("func: start async_func")
    await asyncio.sleep(1)
    print("func: end async_func")

async def main():
    print("main: start main")
    print("main: before func")
    task = asyncio.create_task(async_func())
    await asyncio.sleep(0)
    print("main: after func")
    await asyncio.sleep(3)
    print("main: end main")


if __name__ == '__main__':
    asyncio.run(main())
```

```
main: start main
main: before func
func: start async_func
main: after func
func: end async_func
main: end main
```

`async_func`の開始後に`print("main: after func")`を実行することができました

# 再考：同期関数から同期関数を呼ぶ(fire and forget)

ここまでいろいろ試してきましたが、一周回って同期関数から同期関数を非同期的に呼びたいケースもあるかと思います。（ex. 完了を待つ必要のないファイルの書き出しやDBへの書き込みなど）
もし、同期関数の処理の完了を待たずに次の処理を行いたい場合（いわゆるfire and forget）は、次のようにする必要があります。

```python
import asyncio
import time


def sync_func():
    print("func: start sync_func")
    time.sleep(1)
    print("func: end sync_func")

def main():
    print("main: start main")
    print("main: before func")
    asyncio.new_event_loop().run_in_executor(None, sync_func)
    print("main: after func")
    time.sleep(3)
    print("main: end main")


if __name__ == '__main__':
    main()
```

```
main: start main
main: before func
func: start sync_func
main: after func
func: end sync_func
main: end main
```

同期処理同士で完了を待たない実装を実現できました。

# 参考文献
- [Python 3.10 非同期処理を完了を待たないで実行](https://zenn.dev/3w36zj6/scraps/e9fc9724023389)