概要
最近流行りに流行っているLLMを利用するにあたり、ストリーミングでのレスポンス表示や複数のリクエストによる遅延の防止を目的とし、非同期処理を利用するケースが増えている気がします。ということで、asyncioの挙動を今一度整理してみました。
本記事では、以下の4ケース+αの書き方を記しています
- 同期関数から同期関数を呼ぶ
- 同期関数から非同期関数を呼ぶ
- 非同期関数から同期関数を呼ぶ
- 非同期関数から非同期関数を呼ぶ
同期関数から同期関数を呼ぶ
非同期処理を何も考えない普通のケースです。
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
と非同期化してあげます
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) が完了するまで待ち、そしてイベントループを閉じるという一連の処理を行うためです。
非同期関数から同期関数を呼ぶ
今度は逆に、非同期関数から同期関数を呼んでみます
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が利用されるようです)で実行できるrun_in_executor()
を利用してあげれば良いです
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")
が実行されています
非同期関数から非同期関数を呼ぶ
最後に非同期関数から非同期関数を呼ぶケースですが、これが一番複雑です。
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をつける
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をスケジュールすることができます。
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)
です。
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)は、次のようにする必要があります。
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
同期処理同士で完了を待たない実装を実現できました。