multiprocessing and start methods

threading & multiprocessing · Advanced · 13 min

Start methods explain most cross-platform multiprocessing bugs, from missing globals to hangs.

The multiprocessing module mirrors threading: Process(target=f, args=...), start(), join(), plus Pool with map, imap_unordered and apply_async. Check p.exitcode after join(): 0 means success, negative means killed by a signal.

How a child process starts matters:

  • spawn: start a fresh interpreter that imports your main module, then unpickles the target. Safe and portable, slower to start. Default on Windows and macOS.
  • fork: copy the parent process (POSIX only). Fast, and the child inherits all memory, including globals set at runtime. But forking a process that has threads is unsafe: locks held by other threads are copied in a locked state and can deadlock the child. Default on Linux through 3.13.
  • forkserver: a clean server process forks children on request; safer than fork, faster than spawn. Python 3.14 makes it the default on Linux.

Choose explicitly for predictable behaviour:

ctx = multiprocessing.get_context("spawn")
with ctx.Pool(4) as pool: ...

ProcessPoolExecutor(mp_context=ctx) accepts the same context. Code that "works on Linux but not Mac" usually relied on fork inheriting globals; pass state explicitly as arguments or via an initializer instead.

Example

import multiprocessing as mp
import os

CONFIG = {"mode": "default"}

def show(tag):
    print(f"{tag} child {os.getpid() % 1000}: mode={CONFIG['mode']}")

def square(n):
    return n * n

if __name__ == "__main__":
    CONFIG["mode"] = "changed at runtime"
    print("default method:", mp.get_start_method())   # fork on Linux 3.13, spawn on macOS/Windows
    for method in ("fork", "spawn"):
        ctx = mp.get_context(method)
        p = ctx.Process(target=show, args=(method,))
        p.start(); p.join()
        print(method, "exitcode", p.exitcode)
    with mp.get_context("spawn").Pool(2) as pool:
        print(pool.map(square, range(5)))             # [0, 1, 4, 9, 16]
# fork child ...: mode=changed at runtime   (parent memory copied)
# spawn child ...: mode=default             (module re-imported fresh)

Remember

  • multiprocessing.Process and Pool mirror the threading API
  • spawn re-imports your module; fork copies memory; forkserver forks from a clean server
  • Pick a start method explicitly with get_context()

Watch out for

  • Relying on fork copying runtime-modified globals, then breaking on macOS or Windows where spawn is the default.

Check yourself

  1. Under the spawn start method, what does a child see for a global changed at runtime in the parent's __main__ block?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods