なにかの処理をバックグラウンドスレッドで実行して、アプリケーション終了時にその処理を止めたいことがあります。 たとえばOpenTelemetryのトレースやログを送信するためにスレッドが使われていますが、それらは終了時にバッファリングしているデータを送信してから終了します。
コマンドラインアプリケーションでは、main関数の終了時にバックグラウンドスレッドの終了を呼び出して待つことができます。
# th_main.py import threading import time alive = True def bgmain(): while alive: print("hello") time.sleep(1) th = threading.Thread(target=bgmain) th.start() def main(): global alive try: while True: time.sleep(1) finally: alive = False th.join() main()
実行例:
$ python3 th_main.py hello hello ^CTraceback (most recent call last): File "/Users/inada-n/work/th_main.py", line 27, in <module> main() ~~~~^^ File "/Users/inada-n/work/th_main.py", line 21, in main time.sleep(1) ~~~~~~~~~~^^^ KeyboardInterrupt
しかし、wsgiアプリケーションのようにmain関数が無い場合があります。そのような場合に終了処理を実装するための標準ライブラリとして提供されているのがatexitモジュールです。次のようにすればバックグラウンドスレッドを止められそうに見えます。
# th_atexit.py import threading import time import atexit alive = True def bgmain(): while alive: print("hello") time.sleep(1) th = threading.Thread(target=bgmain) th.start() @atexit.register def on_exit(): global alive print("atexit") alive = False th.join() def main(): while True: time.sleep(1) main()
しかし、このプログラムを止めるためにはCtrl-Cを2回押す必要がありました。コマンドラインプログラムだから停止できましたが、wsgiアプリケーションだとしたら強制的に終了されるまで待ち続けることになります。
$ python3 th_atexit.py hello hello hello ^CTraceback (most recent call last): File "/Users/inada-n/work/th_atexit.py", line 32, in <module> main() ~~~~^^ File "/Users/inada-n/work/th_atexit.py", line 29, in main time.sleep(1) ~~~~~~~~~~^^^ KeyboardInterrupt hello hello ^CTraceback (most recent call last): File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py", line 1542, in _shutdown _thread_shutdown() KeyboardInterrupt: atexit
ateixit が表示されているのが二回目のCtrl-Cの後、最後であることがわかります。 これはatexitが呼ばれるタイミングの問題です。
Py_Finalize(PyRuntimeState *runtime)
// Block some operations. tstate->interp->finalizing = 1; // Wrap up existing "threading"-module-created, non-daemon threads. wait_for_thread_shutdown(tstate); // Make any remaining pending calls. _Py_FinishPendingCalls(tstate); /* The interpreter is still entirely intact at this point, and the * exit funcs may be relying on that. In particular, if some thread * or exit func is still waiting to do an import, the import machinery * expects Py_IsInitialized() to return true. So don't say the * runtime is uninitialized until after the exit funcs have run. * Note that Threading.py uses an exit func to do a join on all the * threads created thru it, so this also protects pending imports in * the threads created via Threading. */ _PyAtExit_Call(tstate->interp);
atexitが呼ばれるのは非daemonスレッドが全て終了してからだということがわかります。
Pythonのドキュメントにはdaemonスレッドについてこう書かれています。
スレッドには "デーモンスレッド (daemon thread)" であるというフラグを立てられます。 このフラグには、残っているスレッドがデーモンスレッドだけになった時に Python プログラム全体を終了させるという意味があります。フラグの初期値はスレッドを生成したスレッドから継承します。フラグの値は daemon プロパティまたは daemon コンストラクタ引数を通して設定できます。
注釈 デーモンスレッドは終了時にいきなり停止されます。デーモンスレッドで使われたリソース (開いているファイル、データベースのトランザクションなど) は適切に解放されないかもしれません。きちんと (gracefully) スレッドを停止したい場合は、スレッドを非デーモンスレッドにして、Event のような適切なシグナル送信機構を使用してください。
https://docs.python.org/ja/3.12/library/threading.html#thread-objects
このドキュメントを読むと、アプリケーション終了時にきちんと終了させるスレッドはデーモンスレッドにするべきでないということになります。しかし、atexitから終了処理を実行する場合はデーモンスレッドにする必要があるので注意が必要です。
先ほどのプログラムをデーモンスレッドに修正してみましょう。
# th_atexit2.py import threading import time import atexit alive = True def bgmain(): while alive: print("hello") time.sleep(1) th = threading.Thread(target=bgmain, daemon=True) # daemonスレッドに th.start() @atexit.register def on_exit(): global alive print("atexit") alive = False th.join() def main(): while True: time.sleep(1) main()
$ python3 th_atexit2.py hello hello hello ^CTraceback (most recent call last): File "/Users/inada-n/work/th_atexit2.py", line 32, in <module> main() ~~~~^^ File "/Users/inada-n/work/th_atexit2.py", line 29, in main time.sleep(1) ~~~~~~~~~~^^^ KeyboardInterrupt atexit
きちんと1回のCtrl-Cでatexitが呼ばれて終了しました。
しかし、たとえば標準ライブラリの concurrent.futures.ThreadPoolExecutor
はdamonスレッドを使っていません。
そのため、 executor.shutdown(wait=True)
を atexit から呼び出すことができません。
この問題は現在コア開発者で話し合いが進められていますが、まだ解決のためのAPIは実装されていません。 今は非デーモンスレッドをバックグラウンドで使うライブラリにモンキーパッチを当てていくか、WSGIサーバーが提供する独自APIを使う必要があります。
例えば uwsgi
には uwsgi.atexit
というAPIがあり、これに登録された処理は Py_Finalize()
を呼び出す前に実行されるのでThreadPoolExecutorを止めることができます。