[python, multiprocessing] multiprocessing使用時の例外に対する挙動

Pythonで並列処理などのために`multiprocessing`を使うが、`multiprocessing`で子プロセスを生成した時の挙動は、通常の関数呼び出しの挙動と異なる点がいくつかある。

# 準備

今回は簡単のため関数`sleep_bug()`を用いる。これは実行中に途中でわざとエラーを発生させるために, `i==5`の時に`1/0`を実行しエラーを発生するようにした関数である。

~~~python
import time

def sleep_bug():
    for i in range(10):
        print('sleeping %d' % i)
        if i == 5:
            1/0
        time.sleep(1)
    return i

sleep_bug()

# output
'''
sleeping 0
sleeping 1
sleeping 2
sleeping 3
sleeping 4
sleeping 5
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-44-d9f02a4cf7f3> in <module>
----> 1 sleep_bug()

<ipython-input-41-26bb27998e63> in sleep_bug()
     12         print('sleeping %d' % i)
     13         if i==5:
---> 14             1/0
     15         time.sleep(1)
     16 

ZeroDivisionError: division by zero
'''
~~~

# 子プロセスがエラーを起こしても、親プロセスはそのまま動き続ける。

通常の関数呼び出しでは、呼び出した関数にエラーが生じると、プログラムはそこで止まる。しかし、Poolを用いて生成した子プロセスに`sleep_bug()`を実行させると、子プロセスはエラーが発生して止まるが、親プロセスではエラーは発生せずに最後まで進んでしまう。

~~~python
from multiprocessing import Pool
p = Pool(1)
r = p.apply_async(sleep_bug)
p.close()
p.join() #子プロセスが終了するまで待つ。
print('Done')

# output
'''
sleeping 0
sleeping 1
sleeping 2
sleeping 3
sleeping 4
sleeping 5
Done
'''
~~~

子プロセスにエラーが生じて止まった時に、親プロセスにもエラーが伝わるようにするには、`r.get()`を用いる。`r.get()`は通常時には子プロセスが終了するのを待ち、子プロセスの戻り値を出力する関数であるが、子プロセスでエラーが起こった場合には、`r.get()`は例外を送出し、親プロセスもそこで止まるようになる。

~~~python
from multiprocessing import Pool
p = Pool(1)
r = p.apply_async(sleep_bug)
p.close()
output = r.get()
print('Done %d' % output)

# 出力
'''
sleeping 0
sleeping 1
sleeping 2
sleeping 3
sleeping 4
sleeping 5
---------------------------------------------------------------------------
RemoteTraceback                           Traceback (most recent call last)
RemoteTraceback: 
"""
Traceback (most recent call last):
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/pool.py", line 121, in worker
    result = (True, func(*args, **kwds))
  File "<ipython-input-41-26bb27998e63>", line 14, in sleep_bug
    1/0
ZeroDivisionError: division by zero
"""

The above exception was the direct cause of the following exception:

ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-50-fb8f5892e1a7> in <module>
      3     r = p.apply_async(sleep_bug)
      4     p.close()
----> 5     output = r.get()
      6     print('Done %d' % output)

/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/pool.py in get(self, timeout)
    655             return self._value
    656         else:
--> 657             raise self._value
    658 
    659     def _set(self, i, obj):

ZeroDivisionError: division by zero
'''
~~~

`Process`を用いた場合には、エラーは表示されるが、親プロセスは最後まで進む。

~~~python
from multiprocessing import Process
p = Process(target=sleep_bug)
p.start()
p.join()
print('Done')

#出力
'''
sleeping 0
sleeping 1
sleeping 2
sleeping 3
sleeping 4
sleeping 5
Process Process-35:
Traceback (most recent call last):
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/process.py", line 297, in _bootstrap
    self.run()
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/process.py", line 99, in run
    self._target(*self._args, **self._kwargs)
  File "<ipython-input-41-26bb27998e63>", line 14, in sleep_bug
    1/0
ZeroDivisionError: division by zero
Done
'''
~~~

# 親プロセスが止まっても子プロセスは動き続ける

下のスクリプトは、子プロセスが実行中２秒待ち、親プロセスは`sys.exit()`でそこで自分自身を終了させるものである。以下の実行例のように、親プロセスは途中で止まっても、子プロセスは動き続ける。

~~~python
from multiprocessing import Pool
p = Pool(1)
r = p.apply_async(sleep)
p.close()
r.wait(2) #子プロセスを2秒待つ
sys.exit()

#出力
'''
sleeping 0
sleeping 1
An exception has occurred, use %tb to see the full traceback.

SystemExit
sleeping 2
sleeping 3
sleeping 4
sleeping 5
sleeping 6
sleeping 7
sleeping 8
sleeping 9
'''
~~~