Support an explicit environment mapping for multiprocessing spawn
还没有人认领这个 Issue。
- 主要语言
- Python
- 星标
- 77.2k
- 派生
- 35.9k
- 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