We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← True Parallelism and the Runtime step 18 of 18
Worker Recycling: max_tasks_per_child Without the Silent Downgrade
A long-running batch worker that grows to 8 GB and gets OOM-killed mid-job is one of the most common multiprocessing incidents, and the fix is a single keyword argument:
ProcessPoolExecutor(max_workers=8, max_tasks_per_child=100)
The worker is retired and replaced after N tasks, so a leak in a C extension,
a caching decorator nobody remembered, or a fragmented allocator gets reset on
a schedule instead of accumulating until the kernel intervenes. Pool spells
the same thing maxtasksperchild.
The detail that is easy to miss
The docs say the feature “is incompatible with the ‘fork’ start method”. What
they do not say loudly is what happens when you do not pass mp_context:
With
max_tasks_per_childand nomp_context, the executor silently usesspawn.
On 3.14 that downgrades your Linux default of forkserver (1.40 ms per
process) to spawn (26.2 ms) — a 19x increase on an operation you have just
told the pool to perform on a schedule. Nothing warns you. Your throughput
drops and the cause is invisible in the code, because the cause is an argument
you did not pass.
The arithmetic you should do once
recycle overhead per task = (process start + initializer) / N
Pick N so that stays under about 1% of task time. Worked examples:
| Method | Start cost | N | Task time | Overhead |
|---|---|---|---|---|
| forkserver | 1.40 ms | 100 | 10 ms | 0.14% — free |
| spawn | 26.2 ms | 50 | 5 ms | 10.5% — you are paying for this |
| spawn | 26.2 ms | 1 | 1 ms | 2620% — a pathology |
The third row is not a straw man. max_tasks_per_child=1 looks like maximum
safety and is how people “fix” a leak they have not diagnosed.
Your task
def solve(
*,
platform: str,
tasks_per_child: int | None,
mp_context: str | None,
task_ms: float,
) -> tuple[StartMethod, tuple[str, ...], float]:
Return the start method that will actually be used, the ordered warning codes, and the recycling overhead as a percentage rounded to two decimals.
-
No recycling (
tasks_per_child is None): use the caller’s context if they gave a valid one, otherwise the platform default —spawnonwin32anddarwin,forkserverelsewhere. No warnings, 0.0 overhead. -
Recycling with no context: warn
"implicit-context-downgrades-to-spawn"and fix it — choose the cheapest recycling-compatible method the platform offers (forkserveron POSIX,spawnon Windows). Naming it explicitly is the entire value of the wrapper. -
Recycling with
mp_context="fork": warn"fork-incompatible-with-recycling"and resolve the same way. -
Recycling with an unrecognised context: warn
"unknown-context"and resolve the same way. -
Whenever overhead exceeds
BUDGET_PCT, append"recycle-overhead-exceeds-budget"last.
Note the darwin case: macOS defaults to spawn, but it does offer
forkserver, and for a recycling pool that is a 19x difference. Defaulting is
not the same as choosing.
The typing lesson
mp_context arrives as str | None — it came from config, an environment
variable, a CLI flag. if mp_context in ("spawn", "forkserver") does not
narrow a str to a Literal, so returning it will be an assignment error.
Write a small _as_method(value: str | None) -> StartMethod | None that
returns literals directly; mypy narrows on the return "spawn" and the rest
of the function type-checks for free. That function is the parse-don’t-validate
boundary between untrusted configuration and your own vocabulary.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.