C#とPython：似て非なるasync/await


# はじめに

C#で経験ありでpythonを始めたときにpythonにもasync/awaitあるじゃんと喜び勇んで使ってみたわけですが、ドキュメント読んだりしてると微妙に挙動が違うような？
C#に思考が引き摺られていると妙に混乱するので書き出してみます。

# 結論

・pythonの非同期は単一スレッドのイベントループが基本です。
・c#と等価な処理を実現しようとするとpython的に泥臭くなります。（pythonとしては低位レイヤの機能により実現されるので）
・`await`で待機する場合は`create_task`であらかじめタスク登録しておきます。
・本当に並列に動かしたければ`run_in_executer`を使いましょう。ただし排他に注意です。

# 基本的な違い
簡単に言うと
・C#
　スレッドベースの非同期
・python
　（基本）単一スレッドのイベントループベースの非同期
です。

C#では`async`関数作って`await`で待てば別スレッドが動いてくれます。

pythonではひとつのスレッドでイベントループ機構を用い、非同期っぽく動くのが基本です。ただ、普通に別スレッドな動作もやろうと思えばできます。低位レイヤとして分類されているのでなるべく使わないでねという感じでしょう。
低位レイヤは`asyncio.get_running_loop`等で取得したイベントループインスタンス使って直接loopのメソッドを呼び出す場合です。
上位レイヤは`asyncio.create_task`等のメソッドを呼び出す場合です。

# イベントループのメリット
シングルスレッドなので排他不要という点ですね。
ただ`await`挿入しつつも排他したい長いブロックがあるときもあります。その場合は`asyncio.Lock`とかで非同期ロックもできます。

# イベントループのデメリット
・本当に並列に実行するわけではない
重い処理を`create_task`でTask化しても後回しになるだけですね。

C#では`await`指定せずにコルーチンを呼び出すだけでいい感じに別スレッドで動いてくれます。
pythonでは`await`指定していないコルーチンは実行時に警告がでて実行されない、という挙動になります。


# 例：C#とpythonの挙動の違い

同じように書いた両方のコードで実際の動きを比較します。末尾にスレッドIDを表示しています。

## C#の場合

```C#:sample.cs
Console.WriteLine("base entry");
static async Task waitAsync()
{
	Console.WriteLine("  async start:" + Environment.CurrentManagedThreadId);
	await Task.Delay(1000);
	Console.WriteLine("  async end:" + Environment.CurrentManagedThreadId);
}
async Task main()
{
	Console.WriteLine(" main entry start:" + Environment.CurrentManagedThreadId);
	await waitAsync();
	Console.WriteLine(" main entry end:" + Environment.CurrentManagedThreadId);
}
Console.WriteLine("base pre:" + Environment.CurrentManagedThreadId);
await main();
Console.WriteLine("base over:" + Environment.CurrentManagedThreadId);
Thread.Sleep(5000);
Console.WriteLine("base finish:" + Environment.CurrentManagedThreadId);

```


```:c#結果
base entry
base pre:1
 main entry start:1
  async start:1・・・★
  async end:5・・・別スレッドで実行
 main entry end:5
base over:5・・・await待機後も別スレッドのまま
base finish:5
```

## pythonの場合

```python:sample.py
import asyncio
import threading
from time import sleep

print(f'base entry:{threading.get_native_id()}')
async def base():
    async def wait_async():
        print(f'  async start:{threading.get_native_id()}')
        await asyncio.sleep(1)
        print(f'  async end:{threading.get_native_id()}')
    async def main():
        print(f' main entry start:{threading.get_native_id()}')
        await wait_async()
        print(f' main entry end:{threading.get_native_id()}')
    print(f'base pre:{threading.get_native_id()}')
    await main()
    print(f'base over:{threading.get_native_id()}')
    sleep(5)
    print(f'base finish:{threading.get_native_id()}')
asyncio.run(base())
sleep(5)

```

```:py結果
base entry:14308
base pre:14308
 main entry start:14308
  async start:14308
  async end:14308
 main entry end:14308
base over:14308
base finish:14308
```

注目は★ですね。
C#では`await`で待機処理が呼びされると別スレッドとして実行されて、完了すると最後のスレッドのまま動き続けます。（元のスレッドに戻す方法もあります）
pythonでは基本的な流れは変わりませんが、すべて単一スレッドで実行されています。


# C#での並列実行：awaitなしでのコルーチン呼び出し

C#で`async`関数を`await`せずに呼び出すと、`async`関数内部の`await`発生時点で呼び出し元は動き出します。完全に並列実行ですね。排他を考慮する必要がでてきます。

```C#:c# main awaitなし
//await main();
main();
```

```:c# main awaitなし
base entry
base pre:1
 main entry start:1
  async start:1
base over:1・・・☆呼び出し元は動き出す
  async end:8・・・別スレッド
 main entry end:8
base finish:1
```

☆の部分のとおり、awaitの完了を待たずにmainが動き出します。


# Pythonでの疑似並列実行：awaitなしでのTask呼び出し

pythonではコルーチン（`async`）を`await`無しで呼ぶことはできません。
`asyncio.create_task`でタスクを立ち上げ、`sleep`ではなく`asyncio.sleep`の`await`にすると同じような動きができます（シングルスレッドですが）。
疑似並列ですね。

```python:python main をcreate_task
#await main()
asyncio.create_task(main())
print(f'base over:{threading.get_native_id()}')
#sleep(5)
await asyncio.sleep(5) # 非同期待機によりmainが動く
```

```:python main をcreate_task
base entry:30444
base pre:30444
base over:30444・・・☆呼び出し元は動き出す
 main entry start:30444・・・タスクも動いてくれる
  async start:30444
  async end:30444
 main entry end:30444
base finish:30444
```


# Pythonでの並列実行：awaitなしでの別スレッド呼び出し

ではC#側と同じように別スレッドにやってもらう場合はと言うと、`run_in_executer`を使用します。これを使うと別スレッドで実行してくれます。

`run_in_executor`は渡された関数を内部で`await`で呼び出すわけではないので、コルーチンのままだと実行されません。`main`のコルーチン（`async`）指定は解除します。
普通の関数内では`await`は呼べないので、`wait_async`はコルーチンのままで無理やり呼んでみました。

```python:python main を別スレッドで
print(f'base entry:{threading.get_native_id()}')
async def base():
    async def wait_async():
        print(f'  async start:{threading.get_native_id()}')
        await asyncio.sleep(1)
        print(f'  async end:{threading.get_native_id()}')
    def main(loop):#asyncを外し、引数でloopを受け取る
        print(f' main entry start:{threading.get_native_id()}')
        future = asyncio.run_coroutine_threadsafe(wait_async(),  loop) # もとループに合流
        future.result()# 同期待ち
        print(f' main entry end:{threading.get_native_id()}')
    print(f'base pre:{threading.get_native_id()}')
    loop = asyncio.get_running_loop() # 現在のloopを取得
    loop.run_in_executor(None, main, loop) # 別スレッド呼び出し
    print(f'base over:{threading.get_native_id()}')
    await asyncio.sleep(5)
    print(f'base finish:{threading.get_native_id()}')
asyncio.run(base())
sleep(5)
```

```
base entry:18640
base pre:18640
base over:18640・・・☆呼び出し元は動き出す
 main entry start:7028・・・別スレッドでmain実行
  async start:18640・・・・元スレッドで非同期ループで実行
  async end:18640
 main entry end:7028
base finish:18640
```


別スレッドなので排他を考慮する必要があります。

また、`main`関数は別スレッドなので元のループに触れません。`asyncio`のメソッドを呼ぶとエラーになります。（派生したスレッドではループを所持していないためです）
別スレッドから元スレッドのコルーチンを呼んで同期をとりたいときは`asyncio.run_coroutine_threadsafe`で安全に非同期呼び出しできます。

```python:別スレッドから元スレッドの非同期処理
future = asyncio.run_coroutine_threadsafe(wait_async(),  loop) # 元スレッド上で実行される
future.result()# 完了待ち
```

`run_in_executor`は`await`することで完了待機もできます。

```python:別スレッドの完了を待つ
await loop.run_in_executor(None, main, loop)
```

# 重い処理があるからasyncioを使う、のは注意

どこまで非同期でやりたいか、が大事です。
後回しにしたいだけなのか、並列で動かしたいのか。
重い（長い）処理がasync/await対応していれば喜ばしいことです。
しかしやむを得ない理由でasync/await対応していない待機処理（socketとか）を使う場合はやはりrun_in_executorの出番になります。