Process (create, start, join)
Basic Process usage
Section titled “Basic Process usage”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")Why join() matters
Section titled “Why join() matters”join() waits for the process to finish.
Without it:
- your program might exit early
- you might read results before they are ready
The four states of a Process object
Section titled “The four states of a Process object”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:
stateDiagram-v2 [*] --> Created: Process(target=f) Created --> Running: start() Running --> Finished: the target returns Running --> Crashed: the target raises Finished --> Joined: join() Crashed --> Joined: join() Joined --> [*]
Measured on CPython 3.14.4, start method spawn, 8 logical CPUs:
| stage | is_alive() | pid | exitcode |
|---|---|---|---|
after Process(...) | False | None | None |
after start() | True | 1952 | None |
after join() | False | 1952 | 0 |
| child raised | False | set | 1 |
Three things follow that catch people out:
pidisNoneuntilstart(). There is no operating system process to have an id.exitcodeisNonewhile the child runs.Nonemeans 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.
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")See it move
Section titled “See it move”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.
Four children, each sleeping 0.5 s, measured on this machine:
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 collectThe first loop is not parallel at all. It is a sequential program that pays the cost of starting four processes for nothing.
The child does not inherit your variables
Section titled “The child does not inherit your variables”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.
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 <- untouchedThe 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.
Check yourself
Section titled “Check yourself”-
You call p = mp.Process(target=f). Before p.start(), what are p.pid and p.exitcode?
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.
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.
-
A child process raises ValueError. What happens in the parent after p.join()?
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.
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.
-
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?
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.
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.
-
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?
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.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.
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”Exercise 1 – Start a Process
Section titled “Exercise 1 – Start a Process”Exercise 2 – Process Pool map()
Section titled “Exercise 2 – Process Pool map()”Exercise 3 – Multiprocessing Queue
Section titled “Exercise 3 – Multiprocessing Queue”pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading