pythonのasyncioで同期処理を非同期的に動かす方法

六角レンチです
作ってるプログラムで非同期処理を使おうと思ってちょっと沼に入りかけてるので気晴らしに記事を書こうと思います
# 環境
linux 6.6.9
python 3.10.13

最近ノーパソ買ってlinux入れて使ってます
windowsよりすごい軽くて感動した
# 同期的に動いちゃうプログラム
例として下のようなプログラムを書きます
```py
import asyncio
import time
import timeit

# IOバウンド(あるいは同期的な)処理
def blocking(t):
    time.sleep(t)
    print("sleeped", t)

async def main():
    print("main start")
    # ここの部分を非同期的に並行に処理したい
    # 〜〜〜〜〜〜〜〜
    for i in range(3):
        blocking(i)
    # 〜〜〜〜〜〜〜〜
    print("main finish")

# 時間の計測
print(timeit.timeit("asyncio.run(main())", number=1, globals=globals()))
```
このプログラムの
```py
    for i in range(3):
        blocking(i)
```
この部分を非同期的に処理します(というかコメントとして書いてあるけど)
## 実行結果
```
main start
sleeped 0
sleeped 1
sleeped 2
main finish
3.009273783005483
```
一回一回実行して待ってるので3秒かかってます
# イベントループを持ってきて実行する方法
多分これが一般的な方だと思います
```py
import asyncio
import time
import timeit

# IOバウンド(あるいは同期的な)処理
def blocking(t):
    time.sleep(t)
    print("sleeped", t)

async def main():
    print("main start")
    # ここの部分を非同期的に並行に処理したい
    # 〜〜〜〜〜〜〜〜
    loop = asyncio.get_running_loop()
    await asyncio.gather(*(loop.run_in_executor(None, blocking, i) for i in range(3)))
    # 〜〜〜〜〜〜〜〜
    print("main finish")

# 時間の計測
print(timeit.timeit("asyncio.run(main())", number=1, globals=globals()))
```
`asyncio.get_running_loop()`で実行中のループを持ってきて`run_in_executor`で走らせる感じ
`run_in_executor`の第一引数は`None`にすることでデフォルトのエクセキュータ(何もしない場合`ThreadPoolExecutor`)を使用します

`asyncio.gather`はまとめて実行するために使ってます
### 実行結果
```
main start
sleeped 0
sleeped 1
sleeped 2
main finish
2.007849339999666
```
# asyncio.to_threadを使う方法
python 3.9で追加された`to_thread`を使う方法です
明確な違いは`to_thread`は高レベルAPIであるということです(`get_running_loop`は低レベルAPI)
```py
import asyncio
import time
import timeit

# IOバウンド(あるいは同期的な)処理
def blocking(t):
    time.sleep(t)
    print("sleeped", t)

async def main():
    print("main start")
    # ここの部分を非同期的に並行に処理したい
    # 〜〜〜〜〜〜〜〜
    await asyncio.gather(*(asyncio.to_thread(blocking, i) for i in range(3)))
    # 〜〜〜〜〜〜〜〜
    print("main finish")

# 時間の計測
print(timeit.timeit("asyncio.run(main())", number=1, globals=globals()))
```
一行でかけるのでスッキリします
ただしエクセキュータは指定できません
（実際はできますが...[^お前]）
### 実行結果
```
main start
sleeped 0
sleeped 1
sleeped 2
main finish
2.0057736759990803
```
# それぞれのメリットとデメリット
### イベントループを持ってきて実行する方法
#### メリット
- エクセキュータを指定できる
#### デメリット
- 変数が一個必要
### asyncio.to_threadを使う方法
#### メリット
- 一行で済む(変数を作らない)
#### デメリット
- エクセキュータを指定できない[^お前]
# まとめ
エクセキュータを指定したい場合(CPUバウンドな処理とか)じゃなければ`to_thread`でいいと思います
# 参考元
- https://docs.python.org/ja/3/library/asyncio.html
- https://blog.aoirint.com/entry/2022/python_asyncio_examples/

<br><br><br><br>
# おまけ
`to_thread`の中身を見てみましょう
```py
async def to_thread(func, /, *args, **kwargs):
    #〜〜〜〜〜〜〜〜〜〜〜
    #〜 to_threadの説明　〜
    #〜〜〜〜〜〜〜〜〜〜〜
    loop = events.get_running_loop()
    ctx = contextvars.copy_context()
    func_call = functools.partial(ctx.run, func, *args, **kwargs)
    return await loop.run_in_executor(None, func_call)
```
つまり`to_thread`はループを持ってきて実行するやつの糖衣構文です
[^お前]: おまけより、よく見てみると`run_in_executor`の第一引数がNoneなので`get_running_loop`でループを取得した後に[`set_default_executor`](https://docs.python.org/ja/3/library/asyncio-eventloop.html#asyncio.loop.set_default_executor)でデフォルト値を書き込めばエクセキュータを指定できることがわかります。
ただ、ループを持ってきている時点で`run_in_executor`を使えばいいし`set_default_executor`に使えるエクセキュータは`ThreadPoolExecutor`(CPUバウンドな処理とかに向いてない方)でなければならないのであまり使いみちはなさそう