PythonMultiprocessing

Multiprocessing

The threading module lets several threads take turns inside a single Python interpreter, but CPython's Global Interpreter Lock (GIL) means only one of those threads ever executes Python bytecode at a given instant — so threading cannot make CPU-heavy work run faster. The multiprocessing module takes a different approach entirely: instead of multiple threads inside one interpreter, it starts multiple full operating-system processes, each running its own independent Python interpreter with its own GIL and its own memory space. That gives you genuine parallelism across CPU cores.

Creating a Process

Python
import multiprocessing as mp

def square(n):
    print(f"square({n}) = {n * n}")

if __name__ == "__main__":
    p1 = mp.Process(target=square, args=(4,))
    p2 = mp.Process(target=square, args=(7,))

    p1.start()
    p2.start()

    p1.join()
    p2.join()

    print("Both processes finished")
square(4) = 16
square(7) = 49
Both processes finished

The API deliberately mirrors threading.Thread: .start() launches it, .join() waits for it to finish. The key difference is underneath — p1 and p2 are entirely separate processes, each with its own copy of the Python interpreter and its own memory, so they can genuinely execute Python code simultaneously on separate CPU cores.

Note
The `if __name__ == "__main__":` guard is not optional decoration here. On some platforms, starting a child process re-imports the main script, and without the guard that would trigger the whole program (including spawning more processes) to run again inside the child. Always put multiprocessing entry-point code behind this guard.
Pool: parallel map over a list of work

Creating individual Process objects by hand is fine for a couple of tasks, but for "run this function over a big list of inputs, spread across my CPU cores," multiprocessing.Pool is much more convenient.

Python
import multiprocessing as mp

def is_prime(n):
    if n < 2:
        return False
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    return True

if __name__ == "__main__":
    numbers = list(range(100_000, 100_050))

    with mp.Pool(processes=4) as pool:
        results = pool.map(is_prime, numbers)

    primes = [n for n, prime in zip(numbers, results) if prime]
    print(primes)
[100003, 100019, 100043, 100049]

pool.map(is_prime, numbers) splits numbers across the pool's worker processes, runs is_prime on each chunk in parallel, and returns the results in the original order — the same mental model as the built-in map(), just spread across multiple CPU cores.

When multiprocessing wins

Checking whether a number is prime is CPU-bound: the process is never waiting on the network or disk, it is purely doing computation. That is exactly the kind of workload where multiprocessing pays off and threading would not, because the GIL would keep all the actual computation on a single core no matter how many threads you created.

The cost of processes
Processes are not free
Threads are cheap: they share the same memory, so creating one is fast and lightweight. Processes are much heavier — each one starts a brand-new Python interpreter with its own memory space, which costs real time and RAM to set up. On top of that, because processes don't share memory, any data you pass to a worker (and any result you get back) has to be serialized (pickled), sent across, and deserialized on the other side. For small, fast tasks, that overhead can easily cost more than the parallelism saves — multiprocessing tends to pay off for larger, genuinely CPU-heavy chunks of work, not tiny ones.
Threading vs. multiprocessing

threading

multiprocessing

Memory usage

Low — threads share one process’s memory

Higher — each process gets its own interpreter and memory space

Impact of the GIL

Limits to one thread executing Python bytecode at a time

Bypassed entirely — each process has its own GIL

Best use case

I/O-bound work: network calls, file/database I/O, waiting

CPU-bound work: number crunching, data processing, image/video work

  • Starting a process is noticeably slower than starting a thread — reuse a Pool across many small tasks instead of creating new processes for each one.

  • Data passed into and returned from worker processes must be picklable — plain functions, numbers, strings, lists, and dicts are fine; things like open file handles or database connections generally are not.

  • If your bottleneck is waiting on the network or disk, reach for threading (or asyncio) first — multiprocessing’s overhead buys you nothing there.

Rule of thumb
Ask whether your program is waiting or computing. Waiting on something external (I/O-bound) → `threading` or `asyncio`. Grinding through computation on the CPU (CPU-bound) → `multiprocessing`.