Python Monthly Topics

Python 3.14の新機能⁠asyncioタスク可視化機能を使ってみよう

福田@JunyaFffです。2025年9月の「Python Monthly Topics」では、Python 3.14で追加されるasyncioタスク可視化機能であるasyncio psコマンド、asyncio pstreeコマンドと、asyncio.print_call_graph()関数、asyncio.capture_call_graph()関数を紹介します。

はじめに

さまざまな機能強化が予定されているPython 3.14の中で、今回筆者が注目するのはasyncioの新しい可視化ツールです。asyncio psコマンド、asyncio pstreeコマンドと、asyncio.print_call_graph()関数やasyncio.capture_call_graph()関数によって、実行中のasyncioタスクの状態を簡単に把握できるようになります。状態とはつまり「どのようにタスクが呼ばれたか」⁠それによってどのタスクを待たせているか」を可視化できるようになります。

asyncio psコマンドの実行イメージ
asyncio psコマンドの実行イメージ

また、既存のツールと異なる点として、以下の特徴があります。

  • 追加のデバッグコードをアプリケーションへ組み込む必要なし
  • 別のプロセスから確認可能なため、オーバヘッドなし

本記事では機能追加の経緯、基本の使い方を中心に紹介します。本機能の動作確認は、2025年9月時点で公開されている最新のPython3.14.0rc3で、macOSにて行っております。DockerのOfficial Imageでも動作確認が可能ですので、興味のある方はぜひ試してみてください。

また、本機能に関する公式ドキュメントは以下になります。

機能追加の経緯

従来のプロファイラ[1]はイベントループ内部のフレームばかりを表示し、タスクやコルーチンの呼び出し関係が追いづらい、という課題がありました。PyCon US 2025セッション「Zoom, Enhance: Asyncio’s New Introspection Powers」で取り上げられていたのが本機能です。

これに対し、Meta社の「本番プロセスを止めずにasyncioタスクを追跡したい」という要望をきっかけに、コア開発スプリントで実装されたことが紹介されていました。

トークセッションの資料は以下をご参考ください。

トークでバナナの着ぐるみを着てはしゃいでいるお二人のコア開発者
トークでバナナの着ぐるみを着てはしゃいでいるお二人のコア開発者。とってもお茶目ですね。

asyncioでタスクの可視化をする方法

asyncioに追加された可視化機能には、大きく2種類の使い方があります。1つはコマンドラインツールとしてpython -m asyncio psコマンド、python -m asyncio pstreeコマンドを使う方法、もう1つはアプリケーション内部からasyncio.print_call_graph()関数、asyncio.capture_call_graph()関数の新規APIを呼び出す方法です。順に見ていきましょう。

コマンドラインツール asyncio ps/pstree

CLIで呼び出すasyncio psコマンドとpstreeコマンドは、すでに実行されているPythonのPID(Process ID)を引数に指定し実行します。実行中のPythonプログラムの外から実行するため、既存コードの変更は不要でオーバーヘッドもほとんどありません。

まずはPython3.14をインストールしてください。Python3.14でシンプルなPythonスクリプトを実行し、コマンドラインツールの動作を確認してみましょう。実行するサンプルコードは以下のとおりです。

シンプルなサンプルコード
import asyncio

async def main():
    await asyncio.sleep(500)

asyncio.run(main())

PythonのPIDを調べるには、OS(Linux/mac)psコマンドを利用します。grepコマンドで絞り込むと便利です。

psコマンドでPIDを調べる
$ ps | grep Python
...
12345 ttys008 0:00.07 ... Python sample.py

コマンドラインツールは、以下のようにして実行します。

asyncio ps、pstreeコマンドの実行例
$ python3.14 -m asyncio ps 12345
... 結果が出力される

$ python3.14 -m asyncio pstree 12345
... 結果が出力される

asyncio psコマンドの出力例を確認してみましょう。

asyncio psコマンドの実行結果
$ python3.14 -m asyncio ps 12345
tid        task id              task name            coroutine stack                                    awaiter chain                                      awaiter name    awaiter id     
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
182457     0x102cf0050          Task-1               sleep -> main                                                                                                         0x0   

python -m asyncio ps PIDは、現在のasyncioタスク一覧を取得し、タスクID・名前・コルーチンのスタック・awaitしているタスクの実行元を表形式で表示します。python -m asyncio pstree PIDは同じ情報をawaitチェーンのツリーとして出力し、循環があれば検知します。

それぞれ出力される項目の説明は以下の通りです。

項目 説明
tid Thread ID(スレッドID⁠⁠。どのスレッドで実行されているタスクかを識別するための番号
task id asyncio内部で割り振られるタスク固有のID。個々のタスクを一意に識別するために用いられる。
task name タスクの名前。asyncio.create_task(coro, name="Task-1")のように明示的に指定可能。未指定の場合は自動生成される。
coroutine stack 現在のタスクがどのコルーチンを実行中かを「スタック」として表したもの。呼び出し元から現在のコルーチンまでの流れを確認できる。
awaiter chain そのタスクがawaitしている対象。どの処理がどの処理を待っているかを示す。
awaiter name タスクが現在待っている別のタスクの名前。処理がどこで止まっているかを知る手がかりになる。
awaiter id awaitしている対象に割り振られた一意のID。複数の対象を区別するために使われる。

シンプルな例なので1行しか出力されていませんが、複数のタスクが存在する場合はそれぞれのタスクについて同様の情報が表示されます。coroutine stackawaiter chainの具体的な例は、後述のサンプルコードで詳しく説明します。

プログラム内部で活用するデバッグ出力用API

CLIだけでなく、アプリケーション内部からタスクの状態を出力するAPIも追加されます。asyncio.print_call_graph()関数は現在(または明示的に指定した)タスクの他タスクとの関係とスタックを標準出力へ表示します。depthで上位フレームのスキップ、limitでスタック深さの制限を指定できます。

asyncio.print_call_graph()使用例
async def debug_task(task: asyncio.Task[Any]) -> None:
    asyncio.print_call_graph(task, depth=1, limit=5)

より柔軟に扱いたいときはasyncio.capture_call_graph()関数を使います。ログに残したり、GUIデバッガへ渡したりする用途に向いています。この関数ではFutureCallGraphオブジェクトを返し、以下の情報を個別に参照できます。

項目 説明 asyncio psでのどの出力に該当するか
future 指定したtaskオブジェクトへの参照 task idやtask name
call_stack FrameCallGraphEntryオブジェクトのタプル coroutine stack
awaited_by FutureCallGraphオブジェクトのタプル awaiter chainやawaiter name

asyncio.capture_call_graph()は以下のように利用可能です。

asyncio.capture_call_graph() 使用例
graph = asyncio.capture_call_graph(task: asyncio.Task[Any], depth=1, limit=5)
for frame in graph.call_stack:
    print(frame)

limit=Noneで全フレーム、limit=0でawait状態のタスクオブジェクトだけを取得できるので、必要な情報量に合わせて使い分けます。これらのAPIは既存コードに組み込んでも負荷が小さいため、問題が再現した瞬間にダンプするといった運用が可能になります。

具体的なコードを可視化してみよう

以下のサンプルコードを用意しました。asyncio psコマンド、pstreeコマンドを試してみましょう。どこのタスクがどのタスクを待っているか、またそのタスクがどのように呼び出されたかを一目で把握できます。

サンプルコードでは、restaurant()からcustomer()を実行しそこからそれぞれwaiter()chef()cooking()の順番に内部でawaitしています。

イメージとしては以下のようなつながりのあるコードです。

タスクのつながりイメージ
タスクのつながりイメージ
レストランを例にしたタスクのつながりのあるサンプル
import asyncio

async def customer():
    await waiter()

async def waiter():
    await chef()

async def chef():
    await cooking()

async def cooking():
    await asyncio.sleep(500)

async def restaurant():
    async with asyncio.TaskGroup() as tg:
        tg.create_task(customer(), name="customer1.0")  # タスク名を customer1.0 に設定
        await asyncio.sleep(1000)

asyncio.run(restaurant())

上記コードをrestaurant.pyとして保存し、以下のように実行します。

python3.14でrestaurant.pyを実行
$ python3.14 restaurant.py

別のターミナルから先ほど実行したPythonプロセスのPIDを確認し、asyncio psコマンドとpstreeコマンドをそれぞれ実行します。⁠task name」⁠coroutine stack」⁠awaiter chain」に注目してください。

restaurant.pyをpsコマンドで確認してみよう
$ ps | grep Python
...
12345 ttys008 0:00.07 ... Python restaurant.py

$ python3.14 -m asyncio ps 12345
tid      task id     task name     coroutine stack                                    awaiter chain        awaiter name    awaiter id
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2897749  0x104e78230 Task-1        sleep -> restaurant                                                                     0x0
2897749  0x104e78410 customer1.0   sleep -> cooking -> chef -> waiter -> customer     sleep -> restaurant  Task-1          0x104e78230

2行出力されていることがわかります。⁠task name」Task-1customer1.0の2行です。Task-1asyncio.runで実行されているrestaurant()関数で、customer1.0restaurant()関数内で生成されたタスクです。

Task-1coroutine stackを確認すると、sleep -> restaurantとなっております。restaurant()関数は、asyncio.TaskGroup()にてawait asyncio.sleep(1000)を実行しているため、このように出力されています。

続いてcustomer1.0を確認してみましょう。⁠coroutine stack」を見ると、customer1.0タスクがsleep -> cooking -> chef -> waiter -> customerという順番でawaitしていることがわかります。

「coroutine stack」でわかるタスクの流れ。customerはcookingを待っている流れがわかる
「coroutine stack」でわかるタスクの流れ

「awaiter chain」「awaiter name」を見てみましょう。awaiterとは ⁠あるタスクの完了を待っている(=awaitしている)タスク」のことです。

awaiter chain

customer1.0「awaiter name」を見ると、Task-1とあります。Task-1asyncio.runで実行されているrestaurant()関数です。つまりcustomer1.0のawaiterはTask-1です。⁠awaiter chain」を見ると、sleep -> restaurantとなっていて、awaiterであるTask-1の状態を確認できます。

続いて、pstreeコマンドを実行してみましょう。

さらに呼び出し元が明確になり、以下のようにツリー形式で表示されます。pstreeコマンドでは、awaitの関係がツリー形式で表示されます。上から順にTask-1restaurant()あることがわかり、customer1.0であるcustomer()関数がwaiter()関数、waiter()関数がchef()関数、chef()関数がcooking()関数を呼んでいて、最終的にsleepしていることが明示されます。

restaurant.pyをpstreeコマンドで確認してみよう
$ python3.14 -m asyncio ps 12345
└── (T) Task-1
    └──  restaurant /home/user/sample/restaurant.py:36
        └──  sleep /usr/lib/python3.14/asyncio/tasks.py:702
            └── (T) customer1.0
                └──  customer /home/user/sample/restaurant.py:5
                    └──  waiter /home/user/sample/restaurant.py:9
                        └──  chef /home/user/sample/restaurant.py:13
                            └──  cooking /home/user/sample/restaurant.py:17
                                └──  sleep /usr/lib/python3.14/asyncio/tasks.py:702

これらの可視化によって、意図的に実行されているか、想定外のところでawaitしていないか、などを簡単に把握できるようになります。特に複雑な非同期処理を扱う場合に有用です。

どのように実現されているかを紹介

この機能がどのように実現されているか少し紹介します。Python 3.14では_asyncio.Future_asyncio_awaited_by属性が追加され、言語仕様として親タスクへの参照を保持するようになりました(FutureオブジェクトはTaskオブジェクトの元となるオブジェクトです⁠⁠。

Taskオブジェクトに_asyncio_awaited_by属性が含まれていることは、Python 3.14のREPLでも確認できます。

_asyncio_awaited_by属性を確認してみよう
$ python3.14 -m asyncio
asyncio REPL 3.14.0rc3 (v3.14.0rc3:1c5b28405a7, Sep 18 2025, 10:24:24) [Clang 16.0.0 (clang-1600.0.26.6)] on darwin
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> async def foo():
...     await asyncio.sleep(1)
...
>>> task = asyncio.create_task(foo())
>>> dir(task)
[... '_asyncio_awaited_by' ...]

まとめ

Python 3.14で追加されたasyncioのタスク可視化機能は、ゼロオーバーヘッドでタスク同士の関係を把握できるはじめての仕組みです。

python -m asyncio ps/pstreeによる外部診断と、capture_call_graph系APIによる内部ダンプを組み合わせれば、これまでブラックボックスだったawait待ちの連鎖を正確に追跡できます。既存の手法では困難だった本番環境でのデバッグやプロファイリングが現実的になりつつあります。正式リリースに向けて、まずは検証環境で新APIを試し、自身のプロジェクトにどう組み込むかを検討してみてください。

最後に私事ですが、本機能についてPyCon JP 2025でも紹介しました。⁠タスクって今どうなってるの?3.14の新機能asyncio psとpstreeでasyncioのデバッグを」というトークです。気になる方はそちらもぜひご参考にしてください。

おすすめ記事

記事・ニュース一覧