Multiprocessing

Python 3.12 ยท โœ“ verified by execution on 2026-07-30

Unlike threading, which is constrained by the Global Interpreter Lock (GIL) for CPU-bound tasks, the multiprocessing package spins up entirely separate Python processes, each with its own interpreter and memory space.

This allows you to bypass the GIL and utilize all CPU cores on your machine, at the cost of higher startup overhead and the need to serialize (pickle) data passed between them.

Process Lifecycle Visualization

Parent process Worker 1 own interpreter + own GIL Worker 2 own interpreter + own GIL pickle -> <- pickle
multiprocessing sidesteps the GIL by giving each worker its own interpreter โ€” but arguments and results must be pickled to cross the process boundary, which is the cost you pay.

Multiprocessing Basics

multiprocessing sidesteps the GIL by not sharing an interpreter at all. Each worker is a genuine OS process with its own interpreter and its own GIL, so Python bytecode really does run on several cores at once.

Spawning a basic process

python
import multiprocessing
import os

def worker(name):
    print(f"Worker {name} running in process")

if __name__ == '__main__':
    p = multiprocessing.Process(target=worker, args=('A',))
    p.start()
    p.join()
Output
Worker A running in process

Memory Isolation (The Catch)

Unlike threads, processes do not implicitly share memory. Each process has its own isolated memory space. Modifying a global variable in a worker will NOT modify it in the parent!

Memory isolation between processes

python
import multiprocessing

shared_list = []

def add_item(item):
    shared_list.append(item)
    print(f"Child: {shared_list}")

if __name__ == '__main__':
    p = multiprocessing.Process(target=add_item, args=(1,))
    p.start()
    p.join()
    print(f"Parent: {shared_list}")
Output
Child: [1]
Parent: []

Using Pools for Data Parallelism

A Pool is the convenient front door: hand it a function and an iterable, and it spreads the work across however many worker processes you asked for, collecting the results in order.

Using Pool to map functions

python
import multiprocessing

def square(x):
    return x * x

if __name__ == '__main__':
    with multiprocessing.Pool(processes=3) as pool:
        results = pool.map(square, [1, 2, 3])
    print(results)
Output
[1, 4, 9]

The Overhead of Processes

Spawning a process means launching an entire Python interpreter. For small tasks, the overhead of creating processes can exceed the benefit of parallelism.

python ยท visualize
import time
import multiprocessing

def heavy_computation(n):
    return sum(i * i for i in range(n))

work_size = 1000

print(f"Running task of size {work_size}...")

start = time.time()
heavy_computation(work_size)
print(f"Single Process Time: {time.time() - start:.4f}s")

start = time.time()
p = multiprocessing.Process(target=heavy_computation, args=(work_size,))
p.start()
p.join()
print(f"Multi Process Time: {time.time() - start:.4f}s")

Pickling Constraints

Nothing is shared for free. Arguments and return values are pickled, shipped across a pipe, and rebuilt on the other side โ€” which is why lambdas and open file handles cannot be sent, and why tiny tasks can end up slower than doing the work in one process.

Pickling constraint for arguments

python
import multiprocessing

def worker(func):
    pass

if __name__ == '__main__':
    try:
        p = multiprocessing.Process(target=worker, args=(lambda: print("hi"),))
        p.start()
    except Exception as e:
        print("PickleError")
Output
PickleError

Check yourself

Why can multiprocessing use several CPU cores when threading cannot?

Reveal answer

Each process has its own interpreter and its own GIL โ€” The GIL is per-interpreter. Separate processes mean separate GILs, so Python bytecode genuinely runs in parallel.

What happens to arguments and return values crossing a process boundary?

Reveal answer

They are pickled and copied โ€” Objects are pickled to cross processes. That copying is the overhead which can make multiprocessing slower for small tasks.

Which workload benefits most from multiprocessing?

Reveal answer

CPU-bound work like image processing or number crunching โ€” CPU-bound work is exactly what the GIL blocks. I/O-bound waiting is better served by threads or asyncio.

Why must the entry point be guarded with if __name__ == main?

Reveal answer

On spawn platforms the child re-imports the module, so unguarded code spawns recursively โ€” Without the guard each child re-executes module-level code and spawns more children โ€” effectively a fork bomb on Windows and macOS.

Challenges

Challenge 1 +100 XP

Use multiprocessing.Pool with 2 processes to compute squares of [1, 2].

python
  • Test 1 โ€” expects "[1, 4]\n"
Need a hint? (โˆ’25% XP)

Pool(2)

Show solution (0 XP)
import multiprocessing

def square(x):
    return x*x

if __name__ == '__main__':
    with multiprocessing.Pool(2) as pool:
        print(pool.map(square, [1, 2]))

Challenge 2 +100 XP

Use a multiprocessing Queue to send the string 'data' from a child process to the parent.

python
  • Test 1 โ€” expects "data\n"
Need a hint? (โˆ’25% XP)

q.put() in child, q.get() in parent

Show solution (0 XP)
import multiprocessing

def sender(q):
    q.put('data')

if __name__ == '__main__':
    q = multiprocessing.Queue()
    p = multiprocessing.Process(target=sender, args=(q,))
    p.start()
    print(q.get())
    p.join()