Repository navigation
gh-155717: Determine multiprocessing's default start method lazily - #159254
Conversation
pythongh-155827 made `import multiprocessing` check whether the temporary directory is writeable in order to choose between "forkserver" and "spawn" as the default start method. That check ran at import time and pulled in `multiprocessing.util`, `subprocess`, `tempfile`, `shutil` and `random`, noticeably slowing down the import for every program, including ones that never start a process and every spawned child, the fork server and the resource tracker. The default start method is now determined the first time it is needed instead: on the first `get_context()`, `get_start_method()`, `get_all_start_methods()` or default `Process` start. `multiprocessing.util` goes back to importing `tempfile` only where it is used. Also include the fix from pythongh-158410 by Vyron Vasileiadis, along with a test adapted from that PR: when `tempfile` finds no usable temporary directory at all, the check now reports that instead of raising `FileNotFoundError`, so that "spawn" is chosen. The regression was reported, and the lazy approach suggested, by Leandro Damascena in python#155717 (comment) Co-authored-by: Vyron Vasileiadis <hi@fedonman.com>
Documentation build overview
|
|
Thanks a lot for picking this up so quickly, and for going with the lazy approach! I ran this PR through the same Lambda benchmarks (x86_64, 1024 MB, PGO+LTO), comparing it with current
The import regression is completely gone, and the cost only shows up once the default start method is actually needed, as expected. Starting a real process even got faster than on LGTM from the benchmark side. Thanks again! |
|
Thanks @gpshead for the PR 🌮🎉.. I'm working now to backport this PR to: 3.15. |
|
Sorry, @gpshead, I could not cleanly backport this to Please backport manually with cherry_picker, see the devguide for more information. |
|
GH-159271 is a backport of this pull request to the 3.15 branch. |
…hod lazily (pythonGH-159254) pythongh-155827 made `import multiprocessing` check whether the temporary directory is writeable in order to choose between "forkserver" and "spawn" as the default start method. That check ran at import time and pulled in `multiprocessing.util`, `subprocess`, `tempfile`, `shutil` and `random`, noticeably slowing down the import for every program, including ones that never start a process and every spawned child, the fork server and the resource tracker. The default start method is now determined the first time it is needed instead: on the first `get_context()`, `get_start_method()`, `get_all_start_methods()` or default `Process` start. `multiprocessing.util` goes back to importing `tempfile` only where it is used. Also include the fix from pythongh-158410 by Vyron Vasileiadis, along with a test adapted from that PR: when `tempfile` finds no usable temporary directory at all, the check now reports that instead of raising `FileNotFoundError`, so that "spawn" is chosen. The regression was reported, and the lazy approach suggested, by Leandro Damascena in python#155717 (comment) (cherry picked from commit 48ddda1) Co-authored-by: Vyron Vasileiadis <hi@fedonman.com>
|
|
|
…zily (GH-159254) (GH-159271) gh-155827 made `import multiprocessing` check whether the temporary directory is writeable in order to choose between "forkserver" and "spawn" as the default start method. That check ran at import time and pulled in `multiprocessing.util`, `subprocess`, `tempfile`, `shutil` and `random`, noticeably slowing down the import for every program, including ones that never start a process and every spawned child, the fork server and the resource tracker. The default start method is now determined the first time it is needed instead: on the first `get_context()`, `get_start_method()`, `get_all_start_methods()` or default `Process` start. `multiprocessing.util` goes back to importing `tempfile` only where it is used. Also include the fix from gh-158410 by Vyron Vasileiadis, along with a test adapted from that PR: when `tempfile` finds no usable temporary directory at all, the check now reports that instead of raising `FileNotFoundError`, so that "spawn" is chosen. The regression was reported, and the lazy approach suggested, by Leandro Damascena in #155717 (comment) (cherry picked from commit 48ddda1) Co-authored-by: Vyron Vasileiadis <hi@fedonman.com> * gh-155717: Reword the NEWS entry for the default start method change Say that the change applies when there is no writable temporary directory and that starting a process previously failed there. --------- Co-authored-by: Vyron Vasileiadis <hi@fedonman.com>
…ly filesystems (GH-155827) (#155886) * gh-155717: use `spawn` as the default start method for read-only filesystems (GH-155827) The "forkserver" start method (the default start method on non-Windows systems) requires the ability to write temporary files, which is not possible if TMPDIR is read-only (e.g., k8s containers mounted with `readOnlyRootFilesystem=True`). On such filesystems, the default start method changes from "forkserver" to "spawn". (cherry picked from commit c144799) Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> * Update Lib/test/_test_multiprocessing.py * [3.14] gh-155717: Determine multiprocessing's default start method lazily (GH-159254) gh-155827 made `import multiprocessing` check whether the temporary directory is writeable in order to choose between "forkserver" and "spawn" as the default start method. That check ran at import time and pulled in `multiprocessing.util`, `subprocess`, `tempfile`, `shutil` and `random`, noticeably slowing down the import for every program, including ones that never start a process and every spawned child, the fork server and the resource tracker. The default start method is now determined the first time it is needed instead: on the first `get_context()`, `get_start_method()`, `get_all_start_methods()` or default `Process` start. `multiprocessing.util` goes back to importing `tempfile` only where it is used. Also include the fix from gh-158410 by Vyron Vasileiadis, along with a test adapted from that PR: when `tempfile` finds no usable temporary directory at all, the check now reports that instead of raising `FileNotFoundError`, so that "spawn" is chosen. The regression was reported, and the lazy approach suggested, by Leandro Damascena in #155717 (comment) (cherry picked from commit 48ddda1) Co-authored-by: Vyron Vasileiadis <hi@fedonman.com> * gh-155717: Reword the NEWS entry for the default start method change Say that the change applies when there is no writable temporary directory and that starting a process previously failed there. --------- Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> Co-authored-by: Vyron Vasileiadis <hi@fedonman.com> Co-authored-by: Gregory P. Smith <greg@krypto.org>
gh-155827 made
import multiprocessingcheck whether the temporary directory is writeable, to choose betweenforkserverandspawnas the default start method. Doing that at import time is a startup regression, reported by Leandro Damascena in #155717 (comment): the import now also loadsmultiprocessing.util,subprocess,tempfile,shutilandrandom, even in programs that never start a process. Spawned children, the fork server and the resource tracker pay it too, as each importsmultiprocessing.This keeps the behaviour of gh-155827 but determines the default start method the first time it is needed, as that comment suggested.
Changes
DefaultContextresolves the platform default on first use (get_context(),get_start_method(),get_all_start_methods(), or starting a defaultProcess) and caches it.get_start_method(allow_none=True)still returnsNonewithout resolving anything.multiprocessing.contextno longer importsmultiprocessing.utilat the top level, andmultiprocessing.utilimportstempfileonly where it is used, as before gh-155717: usespawnas the default start method for read-only filesystems #155827._has_writeable_tempdir()returns false instead of raisingFileNotFoundErrorwhentempfilefinds no usable temporary directory at all. With the lazy change alone that error would only move from import to first use, so both are needed. This PR supersedes gh-155717: Fix importing multiprocessing with no usable temp directory #158410.versionchanged.flowchart TB subgraph before["Before (gh-155827)"] direction TB b1["import multiprocessing"] --> b2["import multiprocessing.util,<br>subprocess, tempfile, shutil, random"] b2 --> b3{"can a temporary file<br>be created?"} b3 -- "yes" --> b4["default = forkserver"] b3 -- "no" --> b5["default = spawn"] b3 -- "no usable temp<br>directory at all" --> b6["import fails with<br>FileNotFoundError"] b4 --> b7["first get_context() /<br>Process.start()"] b5 --> b7 b7 --> b8["use the default<br>chosen at import"] end subgraph after["After (this PR)"] direction TB a1["import multiprocessing"] --> a2["no extra imports,<br>no default chosen yet"] a2 --> a3["first get_context() /<br>Process.start()"] a3 --> a4["import multiprocessing.util<br>and tempfile"] a4 --> a5{"can a temporary file<br>be created?"} a5 -- "yes" --> a6["default = forkserver"] a5 -- "no, or no usable<br>temp directory at all" --> a7["default = spawn"] a6 --> a8["cache and use<br>the default"] a7 --> a8 endThe diagram shows POSIX platforms that support forkserver, other than macOS. Windows and macOS always default to
spawnand never run the check; that is unchanged.Measurements
Linux x86_64,
-O3non-PGO build, whole-process wall time, median of 140 interleaved runs. "Before gh-155827" uses theLib/multiprocessingpackage from the parent of that commit.import multiprocessingfrom multiprocessing import cpu_count; cpu_count()sys.modulesafter the importBehaviour notes
get_all_start_methods(), orget_context()while no start method is set.DefaultContext._default_contextis now afunctools.cached_property. It can still be read, assigned, deleted and patched like the plain attribute it was, andDefaultContext(context)still accepts an explicit default.Backports
This needs backporting to 3.15: the 3.15 backport of gh-155827 (gh-155887) was merged on 2026-10-10, so the 3.15 branch currently has the import-time check, including the
FileNotFoundErroron import when there is no usable temporary directory. The 3.14 backport (gh-155886) is still open and should pick this up before it is merged.There is no new NEWS entry: the entry from gh-155827 is still in
Misc/NEWS.d/next/on both main and 3.15 and still describes the user-visible change. If a release containing gh-155827 goes out before this lands on that branch, this needs its own NEWS entry there.Authored using Claude, reviewed by me.