【Python3】async/awaitを使った非同期/同期処理の学習

仮想環境でjupyter notebookを使いながら、Pythonのasync/awaitを使った非同期処理/同期処理の動作の違いを学習しました。Pythonでasync/awaitが正式に導入されたのは、Python 3.5からです。

Pythonのasync/awaitは、ジェネレータベースのコルーチンモデルを採用しており、async関数はジェネレータになります。
因みにJavaScriptのawait/awaitはPromiseベースのコルーチンモデルを採用しており、async関数はPromiseを返します。

また、async/awaitを使った非同期処理は書き方によって、同期処理(逐次処理)にすることもでき、ある場合は非同期処理、ある場合は同期処理といった非常に柔軟なプログラムの書き方が実現できます。

なかなか理解し難いものなので、時間が経って忘れてしまう前にメモ書きしています。

検証環境

  1. Python : v3.8.10
  2. Jupyter notebook : エディタにVSCodeを使います。環境構築については、Jupyter notebookを使ったPythonの学習をご参照ください。

実行画面

用語について

コルーチンとは

コルーチンとは、いったん処理を中断した後、続きから処理を再開できるものをいいます。関数の前にasyncをつけたものはコルーチンと呼ばれ、コルーチンとして機能させるには必ず前にawaitを付けます。

asyncioとは

asyncio は async/await 構文を使い 並行処理の コードを書くためのライブラリです。

asyncio は、高性能なネットワークとウェブサーバ、データベース接続ライブラリ、分散タスクキューなどの複数の非同期 Python フレームワークの基盤として使われています。

非同期処理

非同期処理の一例

import asyncio  # ←async/await 構文を使い 並行処理のコードを書くためのライブラリ
import time        # ←処理時間を測るために使います

async def mysleep(sec):
    print(f"{sec}秒待ちます")
    await asyncio.sleep(sec)
    print(f"{sec}秒待ち終了")

async def main():
    start=time.perf_counter()
    print(f"開始時間: {time.strftime('%X')}")
 # --asyncio.create_task() 関数でコルーチンを並行して走らせる
    task1=asyncio.create_task(mysleep(1))
    task2=asyncio.create_task(mysleep(2))
    await task1
    await task2
    end=time.perf_counter()
    print(f"終了時間: {time.strftime('%X')}")
    print('main関数終了')
    print('合計時間 {:.3f}'.format(end - start))
    
await main()

補足ですが、最後のawait main()は、本来ならasyncio.run(main())としてイベントループ作成して、コルーチンであるmain()関数を呼び出します。

main()関数の呼び出し方

if __name__ ==  "__main__" :
     asyncio.run(main())

ここではその記述はしていませんが、jupyter notebookでは不要です。
※なんとjupyterそのものがイベントループで動いているそうです。

asyncio.create_task() 関数でコルーチンを並行して走らせることができます。

その結果、2つの処理をバックグラウンドで並列に走らせているので、逐次処理だと3秒かかる所が約2秒で済んでいることが確認できました。

出力結果

開始時間: 11:56:47
1秒待ちます
2秒待ちます
1秒待ち終了
2秒待ち終了
終了時間: 11:56:49
main関数終了
合計時間 2.002

因みにここでawait mysleep(1)とawait mysleep(2)のawaitを消すとどうなるかというと、task終了を待つことなく処理が進みます。

変更箇所のみ

# --省略--
# await task1
# await task2
task1
task2
# --省略--

出力結果

開始時間: 14:02:43
終了時間: 14:02:43
main関数終了
合計時間 0.001
1秒待ちます
2秒待ちます
1秒待ち終了
2秒待ち終了

main関数が先に終了し、その後でtaskが完了しています。

同期処理

同期処理の一例

import asyncio
import time 

async def mysleep(sec):
    print(f"{sec}秒待ちます")
    await asyncio.sleep(sec)
    print(f"{sec}秒待ち終了")

async def main():
    start=time.perf_counter()
    print(f"開始時間: {time.strftime('%X')}")
    await mysleep(1)
    await mysleep(2)
    end=time.perf_counter()
    print(f"終了時間: {time.strftime('%X')}")
    print('合計時間 {:.3f}'.format(end - start))

await main()

mysleep(1)とmysleep(2)を順番に実行(同期処理)しますので、合計時間は1秒処理と2秒処理を足した約3秒かかることが確認できました。

出力結果

開始時間: 14:27:48
1秒待ちます
1秒待ち終了
2秒待ちます
2秒待ち終了
終了時間: 14:27:51
合計時間 3.004

以上です。

Follow me!