Advertisement
🚀 Advanced Python

Python Multiprocessing – True Parallelism for CPU-Bound Tasks

Python multiprocessing bypasses the GIL by running separate Python processes. Each process has its own memory space and Python interpreter, enabling true CPU parallelism. Use multiprocessing when you have CPU-intensive work: number crunching, image processing, machine learning training.

⏱️ 22 min read🎯 Advanced📅 Updated 2026

The Process Class

multiprocessing.Process spawns a new OS process.

Python
from multiprocessing import Process
import os

def worker(name):
    print(f"Worker {name} | PID: {os.getpid()}")

if __name__ == "__main__":  # Required on Windows!
    processes = []
    for i in range(3):
        p = Process(target=worker, args=(i,))
        processes.append(p)
        p.start()

    for p in processes:
        p.join()
▶ Output
Worker 0 | PID: 12345 Worker 1 | PID: 12346 Worker 2 | PID: 12347
💡
Tip

Always put multiprocessing code inside if __name__ == "__main__": to prevent recursive spawning on Windows.

Pool – Parallel Map Operations

Pool distributes work across multiple CPU cores automatically.

Python
from multiprocessing import Pool
import time

def square(n):
    time.sleep(0.1)   # Simulate work
    return n * n

if __name__ == "__main__":
    numbers = list(range(8))

    # Sequential: 0.8s
    # With 4 workers: ~0.2s
    with Pool(processes=4) as pool:
        results = pool.map(square, numbers)

    print(results)
▶ Output
[0, 1, 4, 9, 16, 25, 36, 49]

ProcessPoolExecutor – Modern API

concurrent.futures.ProcessPoolExecutor is the high-level interface for multiprocessing.

Python
from concurrent.futures import ProcessPoolExecutor

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

if __name__ == "__main__":
    inputs = [1_000_000, 2_000_000, 3_000_000]

    with ProcessPoolExecutor(max_workers=3) as exe:
        results = list(exe.map(heavy_computation, inputs))

    print(results)
▶ Output
[333332833333500000, 2666666000000, 9000001500000]
Advertisement

Multiprocessing vs Threading

Choose the right tool for the job.

Python
# I/O bound (waiting for network, disk, DB)
# → Use threading or asyncio
# → Threads share memory, low overhead

# CPU bound (calculation, image processing)
# → Use multiprocessing
# → Each process gets full CPU core
# → Bypasses the GIL

# Rule of thumb:
import os
CPU_COUNT = os.cpu_count()
print(f"Available cores: {CPU_COUNT}")

multiprocessing: Real Parallelism by Dodging the GIL

Because the GIL lets only one thread run Python bytecode at a time, threads can't speed up CPU-heavy work. multiprocessing sidesteps it by launching separate processes — each with its own interpreter, its own GIL, and its own memory — so they truly run on multiple cores.

from multiprocessing import Pool

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

if __name__ == "__main__":          # ← REQUIRED guard on Windows/macOS spawn
    with Pool(4) as pool:           # 4 worker processes
        results = pool.map(heavy, [10_000_000] * 4)   # runs in parallel
threadingmultiprocessing
Memorysharedseparate per process
CPU parallelno (GIL)yes (multi-core)
Best forI/O-boundCPU-bound
Data sharingeasy (same memory)costly (must pickle)

The costs: data passed to workers is pickled (serialized) and copied, so sending huge objects is slow — and anything you send must be picklable (no lambdas, no open files). Because memory isn't shared, use Queue, Pipe, or Manager to communicate. And always wrap the launch in if __name__ == "__main__": — without it, spawn-based platforms re-import your module and fork-bomb. Rule: CPU-bound → processes; I/O-bound → threads/async.

🏋️ Practical Exercise

Run code in parallel processes:

  1. Create and start a Process that runs a function, then join() it.
  2. Use a Pool to map a CPU-heavy function over a list of inputs.
  3. Do the same with ProcessPoolExecutor and executor.map().
  4. Compare the runtime of a CPU-bound task done serially versus across processes.

🔥 Challenge Exercise

Take a CPU-bound task — for example, counting primes up to a large number for several numbers — and run it three ways: serially, with a multiprocessing.Pool, and with a ThreadPoolExecutor. Time all three and explain why processes beat threads here (the GIL). Bonus: use Pool.map with a chunked workload and show how the number of processes affects speedup.

📋 Summary

  • Multiprocessing runs code in separate OS processes, each with its own Python interpreter and memory.
  • Because each process has its own GIL, multiprocessing achieves true parallelism for CPU-bound work.
  • Use the Process class for individual processes, or a Pool to map work across many.
  • ProcessPoolExecutor is the modern, higher-level interface from concurrent.futures.
  • Inter-process communication and process startup add overhead, so favor it for heavy CPU tasks.
  • Share data with Queue, Pipe, or shared-memory objects rather than ordinary variables.

Interview Questions on Multiprocessing

  • What is multiprocessing and how does it differ from multithreading?
  • Why does multiprocessing bypass the GIL?
  • When should you use processes instead of threads?
  • What is a process pool?
  • What is the difference between Pool and ProcessPoolExecutor?
  • What are the downsides of multiprocessing (overhead, IPC)?
  • How do processes share data?

FAQ

When should I use multiprocessing instead of threading? +

Use multiprocessing for CPU-bound work (math, image processing, data crunching), where you need real parallelism across cores. Use threading or async for I/O-bound work, where tasks mostly wait.

How does multiprocessing get around the GIL? +

Each process is a separate Python interpreter with its own GIL, so multiple processes truly run in parallel on multiple CPU cores — unlike threads, which share one GIL within a single process.

Why is multiprocessing slower for small tasks? +

Starting processes and sending data between them (serialization/IPC) has real overhead. For tiny or quick tasks that cost can outweigh any parallel speedup, so multiprocessing pays off mainly for heavy workloads.

How do processes share data? +

Not through ordinary variables, since each has separate memory. Use multiprocessing.Queue, Pipe, Value/Array shared memory, or a Manager for higher-level shared objects.