Skip to content

Process (create, start, join)

basic_process.py
from multiprocessing import Process
 
 
def work(name: str) -> None:
    print("Working in process:", name)
 
 
if __name__ == "__main__":
    p = Process(target=work, args=("p1",))
    p.start()
    p.join()
    print("Main process finished")

join() waits for the process to finish.

Without it:

  • your program might exit early
  • you might read results before they are ready

A Process object exists before the operating system process does, and outlives it. Which attributes are readable depends on where you are in that lifecycle:

diagram Diagram mermaid

Measured on CPython 3.14.4, start method spawn, 8 logical CPUs:

stageis_alive()pidexitcode
after Process(...)FalseNoneNone
after start()True1952None
after join()False19520
child raisedFalseset1

Three things follow that catch people out:

  • pid is None until start(). There is no operating system process to have an id.
  • exitcode is None while the child runs. None means still running, not fine.
  • A child that raises prints its traceback and exits with 1. The parent carries on as if nothing happened — you only find out by checking exitcode.
lifecycle.py
import multiprocessing as mp
 
def boom():
    raise ValueError("child failed")
 
if __name__ == "__main__":
    p = mp.Process(target=boom)
    p.start()
    p.join()
    print(p.exitcode)          # 1   <- the parent survived; nothing was raised here
    if p.exitcode != 0:
        raise RuntimeError("child failed, see traceback above")

Start the workers one way or the other and watch the clock. This is the single most common multiprocessing bug, and it is a bug of ordering, not of syntax.

sketch start-then-join, or start-and-join p5.js
Calling join() inside the same loop as start() waits for each child before launching the next, which runs them one at a time.

Four children, each sleeping 0.5 s, measured on this machine:

join_order.py
ps = [mp.Process(target=slow, args=(0.5,)) for _ in range(4)]
for p in ps:
    p.start()
    p.join()            # 2.82 s  <- waits for each child before starting the next
 
ps = [mp.Process(target=slow, args=(0.5,)) for _ in range(4)]
for p in ps: p.start()  # launch everything first
for p in ps: p.join()   # 0.74 s  <- then collect

The first loop is not parallel at all. It is a sequential program that pays the cost of starting four processes for nothing.

Under spawn — the default on Windows and macOS — the child starts a fresh interpreter and re-imports your module. It does not get a copy of the parent’s current state; it gets whatever the module defines at import time.

not_inherited.py
import multiprocessing as mp
 
counter = 0                      # module level: the child WILL see this
 
def bump():
    global counter
    counter += 100
    print("child sees", counter)  # child sees 100   <- started from 0, not 5
 
if __name__ == "__main__":
    counter = 5                   # inside the guard: the child never runs this
    p = mp.Process(target=bump)
    p.start(); p.join()
    print("parent", counter)      # parent 5   <- untouched

The child printed 100, not 105. It re-imported the module, saw counter = 0, and added 100 to its own copy. The parent’s 5 was never visible to it, and the child’s 100 was never visible back. Two separate memory spaces.

pch.quizTag pch.quizDefaultTitle
  1. You call p = mp.Process(target=f). Before p.start(), what are p.pid and p.exitcode?

    pch.quizShowAnswer

    B — both None, because no operating system process exists yet — The Process object is just a description until start() creates the real process. Measured: pid None and exitcode None before start, pid 1952 and exitcode None while running, exitcode 0 after join.

  2. A child process raises ValueError. What happens in the parent after p.join()?

    pch.quizShowAnswer

    C — nothing is raised; the parent continues and p.exitcode is 1 — The child prints its traceback and exits with code 1. join() does not re-raise, so a parent that never checks exitcode treats a dead worker as a successful one.

  3. Why does 'for p in ps: p.start(); p.join()' take 2.82 s for four 0.5 s children, while starting all then joining all takes 0.74 s?

    pch.quizShowAnswer

    B — calling join() inside the loop waits for each child to finish before the next is started, so nothing overlaps — join() blocks until that child exits. Putting it in the same loop as start() serializes the work while still paying process-creation cost — the worst of both approaches.

  4. counter = 0 at module level; inside the __main__ guard you set counter = 5; the child adds 100 and prints. Under spawn, what does the child print?

    pch.quizShowAnswer

    B — 100 — Spawn starts a fresh interpreter that re-imports the module, so the child sees counter = 0 from module level. Code inside the __main__ guard never runs in the child, so the 5 is invisible to it.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading