8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonAdvent Calendar 2023

Day 24

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

Last updated at Posted at 2023-12-24

概要

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

  1. 同期関数から同期関数を呼ぶ
  2. 同期関数から非同期関数を呼ぶ
  3. 非同期関数から同期関数を呼ぶ
  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_funcasync_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

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

参考文献

8
3
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
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?