3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

はじめに

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#の場合

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の場合

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# 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.sleepawaitにすると同じような動きができます(シングルスレッドですが)。
疑似並列ですね。

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 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で安全に非同期呼び出しできます。

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

run_in_executorawaitすることで完了待機もできます。

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

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

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

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?