これは、なにをしたくて書いたもの?
Pythonで、動作しているスレッドのスタックトレースを出力する方法は?ということで。
環境
今回の環境は、こちら。
$ python3 -V Python 3.8.10
使う標準ライブラリ
今回の目的は、このあたりを使えば良さそうです。
traceback --- スタックトレースの表示または取得 — Python 3.8.10 ドキュメント
sys --- システムパラメータと関数 — Python 3.8.10 ドキュメント
threading --- スレッドベースの並列処理 — Python 3.8.10 ドキュメント
以降、これらを使ってちょっと試していきたいと思います。
スタックトレースを出力するコードを埋め込む
まずは、スタックトレースを出力したい処理自体の中に、スタックトレースを出力するコードを埋め込んでみます。
thread_dump_example.py
import sys import threading import traceback def main(): sub() def sub(): print('===== print traceback#print_stack') traceback.print_stack() print() print('===== print traceback#print_stack(file=sys.stdout)') traceback.print_stack(file=sys.stdout) print() print('===== print traceback#print_stack(sys._current_frames()[threading.get_ident()])') traceback.print_stack(sys._current_frames()[threading.get_ident()]) if __name__ == '__main__': main()
main
→ sub
と呼び出します。
単純には、traceback#print_stack
でOKです。
traceback.print_stack()
出力結果はこちら。
File "thread_dump_example.py", line 26, in <module> main() File "thread_dump_example.py", line 7, in main sub() File "thread_dump_example.py", line 12, in sub traceback.print_stack()
file
パラメーターを使うすることで、スタックトレースの出力先を指定することもできるようです。
traceback.print_stack(file=sys.stdout)
結果は先ほどと同じなので、ここでは割愛。
最後は、少し毛色を変えてsys#_current_frames
を使ってみます。
こちらは、呼び出された時点でのアクティブなスレッドのスタックフレームを含んだ辞書を返す関数です。
この辞書に対して、現在のスレッドの識別子をキーに指定することでスタックフレームを取得できます。
あとは、traceback#print_stack
に渡せばOKです。
traceback.print_stack(sys._current_frames()[threading.get_ident()])
結果は、他と同じになります。
最後に、実行結果全体を載せておきましょう。
$ python3 thread_dump_example.py ===== print traceback#print_stack File "thread_dump_example.py", line 26, in <module> main() File "thread_dump_example.py", line 7, in main sub() File "thread_dump_example.py", line 12, in sub traceback.print_stack() ===== print traceback#print_stack(file=sys.stdout) File "thread_dump_example.py", line 26, in <module> main() File "thread_dump_example.py", line 7, in main sub() File "thread_dump_example.py", line 17, in sub traceback.print_stack(file=sys.stdout) ===== print traceback#print_stack(sys._current_frames()[threading.get_ident()]) File "thread_dump_example.py", line 26, in <module> main() File "thread_dump_example.py", line 7, in main sub() File "thread_dump_example.py", line 22, in sub traceback.print_stack(sys._current_frames()[threading.get_ident()])
プログラム内で動作している、すべてのアクティブなスレッドからスタックトレースを出力する
これで終わってもなんなので、現在のプログラム内で動作しているすべてのアクティブなスレッドからスタックトレースを
出力することを考えてみます。
ここまでに使ってきたAPIと、threadking#enumerate
、Thread.ident
を使えば良さそうです。
複数のスレッドを使い、それぞれが一時的にスリープするプログラムを作成してみます。この時に、各スレッドから
スタックトレースを出力してみましょう。
multi_threaded_dump_example.py
import sys import threading import time import traceback from concurrent.futures import ThreadPoolExecutor def run(): wait() def wait(): current_thread = threading.current_thread() print(f'waiting[{current_thread.ident}] 10sec...') time.sleep(10) def main(): with ThreadPoolExecutor(max_workers=5) as executor: futures = [executor.submit(run) for i in range(5)] print('wait thread dump, 3sec...') time.sleep(3) frames = sys._current_frames() for t in threading.enumerate(): if t.ident in frames: print(f'===== thread[{t.ident}] native_id={t.native_id}, alive={t.is_alive()}, daemon={t.isDaemon()}: dump stack') traceback.print_stack(frames[t.ident]) print() print('wait, all results....') for f in futures: f.result() if __name__ == '__main__': main()
スクリプト本体の方は、main
関数を呼び出し、複数のスレッドを扱います。スレッド数は5つにしました。
def main(): with ThreadPoolExecutor(max_workers=5) as executor: futures = [executor.submit(run) for i in range(5)] ### スタックトレースを出力する処理 print('wait, all results....') for f in futures: f.result()
スタックトレースを出力する部分は後で載せますが、各スレッドの処理が終了するまで待ち合わせは行います。
各スレッドで実行するのは、こちらの関数。
def run(): wait() def wait(): current_thread = threading.current_thread() print(f'waiting[{current_thread.ident}] 10sec...') time.sleep(10)
どのスレッドも10秒待たせるようにしています。
各スレッドを取得し、スタックトレースを出力しているのはこちら。
frames = sys._current_frames() for t in threading.enumerate(): print(f'===== thread[{t.ident}] native_id={t.native_id}, alive={t.is_alive()}, daemon={t.isDaemon()}: dump stack') traceback.print_stack(frames[t.ident]) print()
まずは、アクティブなスレッドのスタックフレームを含む辞書を取得。
frames = sys._current_frames()
次にthreading#enumerate
で、現在アクティブなスレッドのリストを取得。
あとはこのリストをイテレーションしつつ、スタックフレームを含む辞書からThread.ident
を指定して各スレッドの
スタックトレースを出力します。
for t in threading.enumerate(): traceback.print_stack(frames[t.ident])
この時、わかりやすいようにスレッドの識別子、ネイティブなスレッドID、生存中のスレッドか否か、Daemonスレッドか
否かも出力するようにしています。
print(f'===== thread[{t.ident}] native_id={t.native_id}, alive={t.is_alive()}, daemon={t.isDaemon()}: dump stack')
ネイティブなスレッドIDは、LinuxだとLWPのことで良さそうです。
https://github.com/python/cpython/blob/v3.8.10/Python/thread_pthread.h#L322-L349
実行結果。
$ python3 multi_threaded_dump_example.py waiting[140368747820800] 10sec... waiting[140368739428096] 10sec... waiting[140368658036480] 10sec... waiting[140368649643776] 10sec... waiting[140368641251072] 10sec... wait thread dump, 3sec... ===== thread[140368757622592] native_id=28917, alive=True, daemon=False: dump stack File "multi_threaded_dump_example.py", line 42, in <module> main() File "multi_threaded_dump_example.py", line 31, in main traceback.print_stack(frames[t.ident]) ===== thread[140368747820800] native_id=28918, alive=True, daemon=True: dump stack File "/usr/lib/python3.8/threading.py", line 890, in _bootstrap self._bootstrap_inner() File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner self.run() File "/usr/lib/python3.8/threading.py", line 870, in run self._target(*self._args, **self._kwargs) File "/usr/lib/python3.8/concurrent/futures/thread.py", line 80, in _worker work_item.run() File "/usr/lib/python3.8/concurrent/futures/thread.py", line 57, in run result = self.fn(*self.args, **self.kwargs) File "multi_threaded_dump_example.py", line 9, in run wait() File "multi_threaded_dump_example.py", line 15, in wait time.sleep(10) ===== thread[140368739428096] native_id=28919, alive=True, daemon=True: dump stack File "/usr/lib/python3.8/threading.py", line 890, in _bootstrap self._bootstrap_inner() File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner self.run() File "/usr/lib/python3.8/threading.py", line 870, in run self._target(*self._args, **self._kwargs) File "/usr/lib/python3.8/concurrent/futures/thread.py", line 80, in _worker work_item.run() File "/usr/lib/python3.8/concurrent/futures/thread.py", line 57, in run result = self.fn(*self.args, **self.kwargs) File "multi_threaded_dump_example.py", line 9, in run wait() File "multi_threaded_dump_example.py", line 15, in wait time.sleep(10) ===== thread[140368658036480] native_id=28920, alive=True, daemon=True: dump stack File "/usr/lib/python3.8/threading.py", line 890, in _bootstrap self._bootstrap_inner() File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner self.run() File "/usr/lib/python3.8/threading.py", line 870, in run self._target(*self._args, **self._kwargs) File "/usr/lib/python3.8/concurrent/futures/thread.py", line 80, in _worker work_item.run() File "/usr/lib/python3.8/concurrent/futures/thread.py", line 57, in run result = self.fn(*self.args, **self.kwargs) File "multi_threaded_dump_example.py", line 9, in run wait() File "multi_threaded_dump_example.py", line 15, in wait time.sleep(10) ===== thread[140368649643776] native_id=28921, alive=True, daemon=True: dump stack File "/usr/lib/python3.8/threading.py", line 890, in _bootstrap self._bootstrap_inner() File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner self.run() File "/usr/lib/python3.8/threading.py", line 870, in run self._target(*self._args, **self._kwargs) File "/usr/lib/python3.8/concurrent/futures/thread.py", line 80, in _worker work_item.run() File "/usr/lib/python3.8/concurrent/futures/thread.py", line 57, in run result = self.fn(*self.args, **self.kwargs) File "multi_threaded_dump_example.py", line 9, in run wait() File "multi_threaded_dump_example.py", line 15, in wait time.sleep(10) ===== thread[140368641251072] native_id=28922, alive=True, daemon=True: dump stack File "/usr/lib/python3.8/threading.py", line 890, in _bootstrap self._bootstrap_inner() File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner self.run() File "/usr/lib/python3.8/threading.py", line 870, in run self._target(*self._args, **self._kwargs) File "/usr/lib/python3.8/concurrent/futures/thread.py", line 80, in _worker work_item.run() File "/usr/lib/python3.8/concurrent/futures/thread.py", line 57, in run result = self.fn(*self.args, **self.kwargs) File "multi_threaded_dump_example.py", line 9, in run wait() File "multi_threaded_dump_example.py", line 15, in wait time.sleep(10) wait, all results....
それっぽい感じになりましたね。
まとめ
Pythonプログラム内で、スレッドのスタックトレースを出力するようにいろいろ試してみました。
これでシグナルハンドラと合わせて使ったりすると、シグナルでスレッドダンプが出力できたりしそうですが、
どうでしょう。
Pythonの場合は、マルチプロセスで動作していることも多いと思うので、これだけでは足りないでしょうけどね。
今度、もう少しこのあたりの事情も見てみようと思います。