Support an explicit environment mapping for multiprocessing spawn
還沒有人認領這個 Issue。
- 主要語言
- Python
- 星號
- 77.2k
- 分支
- 36k
- PR 合併指標
- PR 指標待擷取
描述
Bug report
Bug description:
"""Linux spawn/environment race. Use --no-environment-updates as a control."""
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
import multiprocessing as mp
import os
import sys
import threading
import time
def child():
pass
def update_environment(stop):
keys = [f"SPAWN_REPRO_{i}" for i in range(256)]
while not stop.is_set():
for key in keys:
os.environ[key] = "test"
for key in keys:
os.environ.pop(key, None)
time.sleep(0)
def start_process(_):
process = mp.get_context("spawn").Process(target=child)
try:
process.start()
except Exception as error:
process.close()
return error
return process
if __name__ == "__main__":
print(sys.version, flush=True)
# Start the resource tracker before racing, using only the public API.
warmup = mp.get_context("spawn").Process(target=child)
warmup.start()
warmup.join()
warmup.close()
stop = threading.Event()
updater = threading.Thread(target=update_environment, args=(stop,))
if "--no-environment-updates" not in sys.argv:
updater.start()
try:
with ThreadPoolExecutor(32) as executor:
for round_number in range(20):
# Finish all starts before joining: avoid concurrent reaping.
results = list(executor.map(start_process, range(32)))
counts = Counter()
for result in results:
if isinstance(result, Exception):
counts[repr(result)] += 1
else:
result.join()
counts[result.exitcode] += 1
result.close()
print(f"round {round_number + 1}: {dict(counts)}", flush=True)
if counts[0] != 32:
raise SystemExit(1)
finally:
stop.set()
if updater.ident is not None:
updater.join()
if you run python test.py, you will get :
round 1: {0: 32}
round 2: {0: 32}
round 3: {0: 32}
round 4: {0: 31, "BrokenPipeError(32, 'Broken pipe')": 1}
and if you run with --no-environment-updates, the subprocess create successfully.
round 1: {0: 32}
round 2: {0: 32}
round 3: {0: 32}
round 4: {0: 32}
round 5: {0: 32}
round 6: {0: 32}
round 7: {0: 32}
round 8: {0: 32}
round 9: {0: 32}
round 10: {0: 32}
round 11: {0: 32}
round 12: {0: 32}
round 13: {0: 32}
round 14: {0: 32}
round 15: {0: 32}
round 16: {0: 32}
round 17: {0: 32}
round 18: {0: 32}
round 19: {0: 32}
round 20: {0: 32}
In my tests on Linux with CPython 3.14, the multiprocessing "spawn" start method follows a vfork() + execv() path. The child shares the parent’s address space until it successfully executes a new program or terminates. During this interval, other threads in the parent can continue modifying the process environment.
With another thread concurrently modifying os.environ, I observed execv() returning EFAULT, followed by the child exiting with status 255. At the Python level, this can surface as a BrokenPipeError from Process.start(). These observations are consistent with a race between the kernel reading the environment passed to execve() and another thread modifying the underlying global environ array.
The C posix_spawn() API accepts an explicit envp argument, and Python’s subprocess.Popen(env=...) similarly allows callers to supply an environment mapping. Could we consider adding an env parameter to multiprocessing.Process for the "spawn" start method, for example mp.get_context("spawn").Process(target=child, env=child_env)? This would allow callers to provide a stable environment for the child interpreter before exec, without temporarily modifying the process-global environment.
CPython versions tested on:
3.14
Operating systems tested on:
Linux
Linked PRs
- gh-157656
貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
研究方向
首先,分別在帶有和不帶 --no-environment-updates 的情況下執行提供的 reproducer test.py,然後追蹤報告中描述的 multiprocessing spawn Process.start 路徑。當 spawn 程序能夠接收明確且穩定的環境映射,且 reproducer 無需依賴程序全域環境更新即可成功完成時,即表示完成。
由索引模型根據 Issue 內容生成。
評估
- 技術堆疊
- linux, python
- 領域
- operating-systems
- Issue 類型
- 功能
- 難度
- 5/5
- 預估耗時
- 一週以上
- 活躍度
- 停滯
- 描述清晰度
- 基本清楚
- 新手友好度
- 30/100