これは DeNA Advent Calendar 2019 の10日目のエントリーです。
9日目は jukey17 さんの Google.Protobuf.Reflectionを利用してC#でProtocol Buffersを汎用的に解析する話 でした。
本エントリに登場するサンプルのコードは次の環境で動作を確認しています。
macOS: 10.15.1
Python: 3.8.0
リポジトリ: https://github.com/tsuyukimakoto/chore_multiprocessing_py38
さて、Pythonには GIL(Global Interpreter Lock) という制限があるため、マルチコアを活かすためにマルチスレッドではなくてマルチプロセスをよく使います。
標準モジュールに multiprocessing があり、手軽に使えるため重宝しているのですが、先日リリースされた Python3.8でデフォルトのサブプロセス開始方法が変わり ました(macOSのみ)。
これまでは fork だったデフォルトのサブプロセス開始方法が 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などを利用しましょう。
ForkしてからexecするまでのルールがmacOS 10.13 (High Sierra) で変わった
python.orgからダウンロードできるインストーラーはファイル名(python-3.8.0-macosx10.9.pkg)から推測すると10.9sdkでビルドされてそうではある。 10.9sdkでビルドされているのは、できるだけ多くの環境で動くようにしたからなどいくつかの理由があるようだ。
forkについては詳解UNIXプログラミング[第3版]などを参照した
Pythonの問題ではない。 スクリプト言語の場合は陥りがち なのだけれど
11日目は rantaro さんの 【3日で実装・公開】エモいアートな画像生成アプリ開発 のようです。