Python システム・環境

Python の ThreadPoolExecutor は max_workers を適切に指定しないとゾンビのプロセスが生まれるかもしれない

注意:この記事は情報に誤りがあるかもしれない

Python の ThreadPoolExecutor は基本的に with 内で動かすが、実際のスレッド数を超える値を max_workers に指定すると予想外のことが起きる。

例えば 3 つのプロセスを並列化したいとき、max_workers を 4 にすると、4 - 3 = 1 のゾンビ・プロセスが生まれる。全体のプログラムがデーモン化されている場合、この 1 個のスレッドが with で回収されない。

例えば gunicorn で動かす Django または Flask のプログラム内に ThreadPoolExecutor で並列化した関数があるとする。この ThreadPoolExecutor で max_workers を関数の数よりも多く設定していると

13474  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13510  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13511  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13512  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13513  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13514  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13549  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13550  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13551  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13552  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13553  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13578  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13579  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13580  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13581  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13582  gunicorn --bind 127.0.0.1:8080 main:app --daemon
13586  gunicorn --bind 127.0.0.1:8080 main:app --daemon

となる。これらのゾンビ・デーモンはシグナルで強制終了されない限り消えない。

デーモンになっていないプログラム、例えばローカルで動かしている Flask では max_workers の値にかかわらず、余ったスレッドは with で消される(たぶん)。

また ThreadPoolExecutor は OS に依存するため、実際は Pebble などを使う。Python の並列化でシグナルを直接扱っているソースコードをよく見かけるが、たぶん避けたほうがいい。