2025年9月: 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タスクの状態を簡単に把握できるようになります。 状態とはつまり「どのようにタスクが呼ばれたか」「それによってどのタスクを待たせているか」を可視化できるようになります。

../_images/ps-sample.png

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

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

  • 追加のデバッグコードをアプリケーションへ組み込む必要なし

  • 別のプロセスから確認可能なため、オーバヘッドなし

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

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

リリースノート:https://docs.python.org/3.14/whatsnew/3.14.html#asyncio-introspection-capabilities 公式ドキュメント:https://docs.python.org/3.14/library/asyncio-graph.html

機能追加の経緯

従来のプロファイラ[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(): https://docs.python.org/3.14/library/asyncio-graph.html#asyncio.print_call_graph

  • asyncio.capture_call_graph(): https://docs.python.org/3.14/library/asyncio-graph.html#asyncio.capture_call_graph

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 しています。

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

../_images/chain-task.png

タスクのつながりイメージ

レストランを例にしたタスクのつながりのあるサンプル
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していることがわかります。

../_images/coroutine-stack.png

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

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

../_images/task-awaiter.png

customer1.0 の 「awaiter」 は、 呼び出し元である Task-1Task-1 は、customer1.0 をawaitしている。

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のデバッグを」 というトークです。気になる方はそちらもぜひご参考ください。

タスクって今どうなってるの?3.14の新機能 asyncio ps と pstree でasyncioのデバッグを | PyCon JP 2025