chakokuのブログ(rev4)

テック・コミック・DTM・・・ごくまれにチャリ

Pythonのasyncioを理解。。取り組み中

JSのasync/awaitにおいて、背景にあるのはPromiseであると理解。JSのPromiseは非常に深い概念のようでさっくり読めるレベルではない。そこで、、「callback実装の苦闘の末、シンプルに表現できるasync/awaitが発明されたのだ」という理解にとどめて・・、改めてPythonのドキュメントを読んだ。同時実行されるtaskのサンプルを作った。

#!/usr/bin/python3

import asyncio
import time

async def greet(id,t):
   print(f"[T_{id}_1] vvvvv  hello  vvvvv")
   print(f"[T_{id}_2] wait({t}) ")
   await asyncio.sleep(0)
   print(f"[T_{id}_3] exit wait")
   print(f"[T_{id}_4] AAAA  bye  AAAA")
   print(f"[T_{id}_5] vvvvv  hello  vvvvv")
   print(f"[T_{id}_6] wait({t}) ")
   await asyncio.sleep(0)
   print(f"[T_{id}_7] exit wait")
   print(f"[T_{id}_8] AAAA  bye  AAAA")


async def main():

   print("create task_m")
   task_m = asyncio.create_task(greet('m',0))
   print("create task_n")
   task_n = asyncio.create_task(greet('n',0))

   print("[M1] sleep(0)")
   await asyncio.sleep(0)
   print("[M2] sleep(0)")
   await asyncio.sleep(0)
   print("[M3] sleep(0)")
   await asyncio.sleep(0)
   print("exit of main")


M = main()
asyncio.run(M)

上記ソースを実行すると以下の出力となる

$ ./test3.py
create task_m
create task_n
[M1] sleep(0)
[T_m_1] vvvvv  hello  vvvvv
[T_m_2] wait(0)
[T_n_1] vvvvv  hello  vvvvv
[T_n_2] wait(0)
[M2] sleep(0)
[T_m_3] exit wait
[T_m_4] AAAA  bye  AAAA
[T_m_5] vvvvv  hello  vvvvv
[T_m_6] wait(0)
[T_n_3] exit wait
[T_n_4] AAAA  bye  AAAA
[T_n_5] vvvvv  hello  vvvvv
[T_n_6] wait(0)
[M3] sleep(0)
[T_m_7] exit wait
[T_m_8] AAAA  bye  AAAA
[T_n_7] exit wait
[T_n_8] AAAA  bye  AAAA
exit of main

どう処理が流れているのかわかりにくいので、、ソース行単位にマークしてみたのが以下

await asyncio.sleep(0)に出くわした時に実行行の切り替わりが発生しているのが分かる。

他のサンプルも含めて理解したのは以下(正しいかどうかは不明)

  1. asyncioの動作はシングルスレッドであり、マルチスレッドではない(スケジューラもいない)
  2. タスクを生成しても勝手には走らない(プリエンプティブな動作はしない)、明示的なタスク切り替えが必要(await文)
  3. メインループ、タスク間は勝手には切り替わらない。切り替わりのきっかけは実行中のスレッド(タスク?)でのawait文の出現か、他のタスクの終了

await文の理解(理系とは思えない情緒的な表現ですが)

  • 自分は今から項(引数というのか)の処理待ちに入ります。非同期処理として待つ*1ので、Pythonの実行を譲ります。他のタスクで処理待ちの人がいたらそっちを実行してね
  • 自分の次の行は、項(引数というのか)の処理が終わってから(処理待ちが完了後)実行します。ただし・・・↓↓↓
  • sleep等の実行が完了してもタスク切替が行われるまでは続く行を実行しません(処理待ちが完了しても勝手には実行再開しません)

タスクと言っていいのか、スレッドと言うべきか、実行行と言うべきなのか。。

■おまけ
await文では指定した項がタスクの場合、await文で指定されたタスクに実行が移るとは限らない。別のタスクに移る場合もある。

#!/usr/bin/python3

import asyncio
import time

async def task1():
   print("T1_1 start")
   print("T1_2 sleep")
   await asyncio.sleep(0)
   print("T1_3 end")


async def task2():
   print("T2_1 start")
   #while True:
   for _ in range(3):
       print("T2_2  loop....")
       time.sleep(1)     # not switch other task
   print("T2_3 sleep")
   await asyncio.sleep(0)
   print("T2_4 end")

async def main():

   print("M_1 create t1")
   t1 = asyncio.create_task(task1())
   print("M_2 sleep")
   await asyncio.sleep(0)    # switch task1
   print("M_3 create t2")
   t2 = asyncio.create_task(task2())    # switch from task1
   print("--------- all tasks ------")
   print(asyncio.all_tasks())
   print("M_4 await t2")
   await t2               # not switch to task2
   print("M_5 await t1")
   await t1               # switch to task2
   print("M_5 end")

asyncio.run(main())

上記サンプルの場合、 await t2の行で、 タスクt2の実行が再開されて、タスクの実行が終わるまで待つ・・・と解釈してしまいますが、、await t2は t2が終わるまで非同期で待つという意味で、実行をt2に切り替えてt2が終わるのを待つという仕様ではないようです。
↓↓↓そのように判断した根拠(以下の実行結果より)↓↓↓

$ ./test4.py
M_1 create t1
M_2 sleep
T1_1 start
T1_2 sleep
M_3 create t2
--------- all tasks ------
{<Task pending name='Task-1' coro=<main() running at ./test4.py:32> cb=[_run_until_complete_cb() at /usr/lib/python3.8/asyncio/base_events.py:184]>, 
<Task pending name='Task-3' coro=<task2() running at ./test4.py:13>>, 
<Task pending name='Task-2' coro=<task1() running at ./test4.py:9>>}
M_4 await t2          #<<<< await t2としているのに、
T1_3 end                # 制御はt1に移った。
T2_1 start
T2_2  loop....
T2_2  loop....
T2_2  loop....
T2_3 sleep
T2_4 end
M_5 await t1
M_5 end

■追記(2023/8/15)
改めて読み直して、、非同期プログラミングはシングルスレッドなのだから、タスクという言葉を使ってはいかんのではないか。実行単位はスレッドというべきではないか。

*1:非同期で待つ・・・CPUをつかんだ状態では待たない。CPUの実行を手放して待つので、待っている間は他の実行待ちになっている処理を再開して実行してもらえれば良い