intellista

engineer's notes about application development, data analysis, and so on

Pythonのスレッドで発生した例外は親スレッドでキャッチできない


こんにちは!

Pythonのスレッドで発生した例外を親スレッドでキャッチできないことを知ったので、記事に致します。

何を言っているのでしょうか?

プログラムで「例外」といえば、何もしないか、ちゃんとthrowすれば、発生した例外が上位の処理に伝搬していきます。
イメージを書くと、次のように、呼び出し先で発生した例外を呼び出し元に伝搬でき、呼び出し元でキャッチできます。

親モジュール: 「例外A」をcatchする ←「例外A」をキャッチできる
↑「例外A」が伝搬する
  子モジュール: 「例外A」をcatch & throwする
    ↑「例外A」が伝搬する
    孫モジュール: 「例外A」をcatch & throwする
      ↑「例外A」が伝搬する
      ひ孫モジュール: 「例外A」が発生する


しかし、少なくともPythonでは、子スレッドの例外は親スレッドに伝わらないことがわかりました。
次のようなイメージです。

親スレッド: catch 例外  ←「例外A」をキャッチできない
  <ここで例外の伝搬が途切れる>
  子スレッド: catch & throw 例外 ←「例外A」をキャッチできる
    ↑「例外A」が伝搬する
    子スレッドの子モジュール: 「例外A」をcatch & throwする
      ↑「例外A」が伝搬する
      子スレッドの孫モジュール: 「例外A」が発生する

そこで今回は、Pythonのスレッドで発生した例外は親スレッドでキャッチできない点についてご説明いたします。

※もしかするとプログラム言語の仕様に依存するかもしれません。本記事ではPython言語についてご説明します。

説明のための処理の構造

Pythonのスレッドで発生した例外は親スレッドでキャッチできないことを説明するための例とする処理の内容は、次のとおりです。

import threading

def func():
  a = 1/0  # raise ZeroDivisionError

def main():
  thread = threading.Thread(target=func)
  thread.start()

main()
  • func()は子スレッドの処理内容です
  • main()は子スレッドを生成して起動しています
  • 最上位のスコープ(mainスレッド)でmain()を呼び出しています

実行結果

Exception in thread Thread-74:
Traceback (most recent call last):
  File "/usr/lib/python3.7/threading.py", line 926, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.7/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "", line 4, in func
    a = 1/0
ZeroDivisionError: division by zero

実験

Pythonのスレッドで発生した例外が親スレッドでキャッチできないことを確認します。

親スレッドでスレッドの例外をキャッチできない

子スレッドの例外を、親スレッド(mainスレッド)でキャッチしてみます。

import threading

def func():
  try:
    print("1")
    a = 1/0
  except Exception as e:
    print("2")
    print("e:", e)
    raise

def main():
  thread = threading.Thread(target=func)
  thread.start()
  print("3")

try:
  main()
except Exception as e:
  print("4")
  print("e:", e)
  raise

実行結果

1
2
e: division by zero
3
Exception in thread Thread-77:
Traceback (most recent call last):
  File "/usr/lib/python3.7/threading.py", line 926, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.7/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "", line 6, in func
    a = 1/0
ZeroDivisionError: division by zero

残念ながら、親スレッド(mainスレッド)でキャッチできたら「4」が表示されるはずですが、表示されません。
つまり、親スレッド(mainスレッド)では、子スレッドの例外をキャッチできないことがわかりました。

子スレッドの終了を待ってもキャッチできない

子スレッドの終了を待ったらキャッチできるのでしょうか?

import threading

def func():
  try:
    print("1")
    a = 1/0
  except Exception as e:
    print("2")
    print("e:", e)
    raise

def main():
  thread = threading.Thread(target=func)
  thread.start()
  thread.join()  # 子スレッドの終了を待つ
  print("3")

try:
  main()
except Exception as e:
  print("4")
  print("e:", e)
  raise

実行結果

1
2
e: division by zero
3
Exception in thread Thread-78:
Traceback (most recent call last):
  File "/usr/lib/python3.7/threading.py", line 926, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.7/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "", line 6, in func
    a = 1/0
ZeroDivisionError: division by zero


残念ながら、親スレッド(mainスレッド)でキャッチできたら「4」が表示されるはずですが、表示されません。
つまり、子スレッドの終了を待っても、親スレッド(mainスレッド)では、子スレッドの例外をキャッチできないことがわかりました。

子スレッド内の例外は子スレッドでキャッチできる

もちろん、 子スレッド内で発生した例外は、子スレッドの最上位ではキャッチできます(通常の例外の伝搬のため)。

import threading

def func():
  try:
    print("1")
    func2()
  except Exception as e:
    print("2")
    print("e:", e)
    raise

def func2():
  print("1-1")
  a = 1/0

def main():
  thread = threading.Thread(target=func)
  thread.start()
  print("3")

try:
  main()
except Exception as e:
  print("4")
  print("e:", e)
  raise

実行結果

1
1-1
2
e: division by zero
3
Exception in thread Thread-17:
Traceback (most recent call last):
  File "/usr/lib/python3.7/threading.py", line 926, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.7/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "", line 6, in func
    func2()
  File "", line 14, in func2
    a = 1/0
ZeroDivisionError: division by zero

ではどうするか?

子スレッドの例外は、次のように「子スレッドで処理する」ほかありません。

import threading

def func():
  try:
    a = 1/0
  except Exception as e:
    do_something() # 処理する!

def main():
  thread = threading.Thread(target=func)
  thread.start()

main()

補足(発見した背景)

親スレッドでまとめて例外を処理できる(すべての処理を止める、ログに出力する、など)と想定していたのですが、親スレッドでは子スレッドの例外を検知できないので、子スレッドの最上位で例外を処理する必要があることがわかりました。

スレッドを使わなければ、例外を再スローしたら上位に伝搬できます。
しかしスレッドを使うとスレッドの最上位で伝搬が終わります。
知らないとハマるポイントかと体感しました。

まとめ

今回は、Pythonのスレッドで発生した例外は親スレッドでキャッチできない点についてご説明いたしました。

なお、スレッドも非常に重要ですが、それ以外でもPythonにはコツが必要なシーンが多々あります。
Python、Pandas、データ分析、に関するコツなどを次の記事にまとめてありますので、是非読んでみてください!
intellista.hatenablog.com