multiprocessingとグローバル変数

2019/12/10 01:00

※ 商品のリンクをクリックして何かを購入すると私に少額の報酬が入ることがあります【広告表示】

これは DeNA Advent Calendar 2019 の10日目のエントリーです。

9日目は jukey17 さんの Google.Protobuf.Reflectionを利用してC#でProtocol Buffersを汎用的に解析する話 でした。

動作環境など

本エントリに登場するサンプルのコードは次の環境で動作を確認しています。

Python3.8(macOS)のmultiprocessingについて

さて、Pythonには GIL(Global Interpreter Lock) という制限があるため、マルチコアを活かすためにマルチスレッドではなくてマルチプロセスをよく使います。

標準モジュールに multiprocessing があり、手軽に使えるため重宝しているのですが、先日リリースされた Python3.8でデフォルトのサブプロセス開始方法が変わり ました(macOSのみ)。

これまでは fork だったデフォルトのサブプロセス開始方法が spawn に変更されたのです。

プラグインのロードなど、比較的生成に時間がかかるものをグローバル変数に格納してサブプロセスから共有していた場合などは動作が変わりました。

spawnではグローバル変数が消える

グローバル変数を利用する

まずは、Python3.7までのデフォルトサブプロセス開始方法だった forkを指定して 、グローバル変数を利用しているスクリプトを実行してみます。

 $ python3.8 use_global.py fork
 INFO:__main__:set_start_method: fork
 INFO:__main__:plugin loaded.
 INFO:__main__:['func0: 0', 'func1: 1', 'func0: 2', 'func1: 3', 'func0: 4', 'func1: 5', 'func0: 6', 'func1: 7', 'func0: 8', 'func1: 9']

次に、サブプロセス開始方法をデフォルトのspawnのまま、同じスクリプトを実行してみます。

 $ python3.8 use_global.py
 INFO:__main__:plugin loaded.
 multiprocessing.pool.RemoteTraceback:
 """
 Traceback (most recent call last):
 File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/multiprocessing/pool.py", line 125, in worker
     result = (True, func(*args, **kwds))
 File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/multiprocessing/pool.py", line 48, in mapstar
     return list(map(*args))
 File "/Users/makoto/projects/chore_multiprocessing_py38/use_global.py", line 29, in process
     return plugin_registry.process(remainder, i)
 AttributeError: 'NoneType' object has no attribute 'process'
 """

macOSのPython3.8からデフォルトになった spawn はサブプロセスの起動に新しいインタプリタを起動します。 グローバル変数は引き継がれず、plugin_registryグローバル変数はNoneのままになっており、 AttributeError が発生します。

グローバル変数ではなく引数で渡す

新しく起動したインタプリタに対して、対象の関数と引数のタプルがpickleされたものが渡されるので、グローバル変数にしておく代わりに 引数として渡して あげれば動作します。

 $ python3.8 use_args.py
 INFO:__main__:plugin loaded.
 INFO:__main__:['func0: 0', 'func1: 1', 'func0: 2', 'func1: 3', 'func0: 4', 'func1: 5', 'func0: 6', 'func1: 7', 'func0: 8', 'func1: 9']

インタプリタの起動について、pdbでコマンドっぽい変数の中身を確認してみたところ、次のような状態でした。

 > /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/multiprocessing/popen_spawn_posix.py(58)_launch()
 cmd = spawn.get_command_line(tracker_fd=tracker_fd,
                                 pipe_handle=child_r)
 (Pdb) cmd
 ['/Library/Frameworks/Python.framework/Versions/3.8/bin/python3.8', '-c', 'from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=8, pipe_handle=19)', '--multiprocessing-fork']

変数の変更を含んだ共有をプロセス間でしたい場合

グローバル変数を共有した気になっていたforkモデルでもメモリはCOWなので、変数への変更はプロセス間で共有されません。

変数の変更を含んだ共有をプロセス間でしたい場合には、素直にmultiprocessing.Valueやmultiprocessing.Managerなどを利用しましょう。

参考

明日の DeNA Advent Calendar 2019 は

11日目は rantaro さんの 【3日で実装・公開】エモいアートな画像生成アプリ開発 のようです。

Prev Entry

Next Entry