python
multiprocessing
multithreading
parallel-computing
concurrency

What am I missing in python-multiprocessing/multithreading?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

The usual thing people are missing when comparing Python multithreading and multiprocessing is the Global Interpreter Lock, or GIL. Threads in CPython are excellent for overlapping I/O, but they do not generally let CPU-bound Python code run in parallel on multiple cores.

Processes do. That is why the right answer is often: use threads for I/O-bound work, use processes for CPU-bound work, and do not expect them to solve the same problem equally well.

Why Threads Often Disappoint for CPU Work

In CPython, only one thread can execute Python bytecode at a time because of the GIL. So code like this may use multiple threads, but not actually speed up a CPU-heavy pure-Python loop.

python
1import threading
2
3def work(n):
4    total = 0
5    for i in range(n):
6        total += i * i
7    return total
8
9threads = [threading.Thread(target=work, args=(10_000_000,)) for _ in range(4)]
10for t in threads:
11    t.start()
12for t in threads:
13    t.join()

This creates concurrency, but not true multi-core execution for ordinary Python bytecode.

Where Threads Shine

Threads are still very useful when the program spends much of its time waiting on external systems such as:

  • network requests
  • disk I/O
  • database calls
  • sleeping or timers

While one thread waits, another can make progress.

python
1import threading
2import time
3
4def fetch(name):
5    print(f"start {name}")
6    time.sleep(1)
7    print(f"done {name}")
8
9threads = [threading.Thread(target=fetch, args=(f"job-{i}",)) for i in range(3)]
10for t in threads:
11    t.start()
12for t in threads:
13    t.join()

For this kind of workload, threads can reduce overall wall-clock time nicely.

Why Processes Help CPU-Bound Work

multiprocessing launches separate processes, each with its own Python interpreter and memory space. That avoids the GIL bottleneck between those processes and allows true parallel CPU execution.

python
1from multiprocessing import Pool
2
3def work(n):
4    total = 0
5    for i in range(n):
6        total += i * i
7    return total
8
9if __name__ == "__main__":
10    with Pool(4) as pool:
11        results = pool.map(work, [10_000_000] * 4)
12    print(results)

This is the standard answer for CPU-heavy pure-Python workloads that need multiple cores.

Processes Have Their Own Cost

Processes are not free. They have more overhead than threads because they do not share memory the same way. Sending large objects between processes can be expensive.

So the rule is not "multiprocessing is always better." It is "multiprocessing is usually better when the CPU work is heavy enough to justify the extra overhead."

Shared State Is Different

Threads share memory by default, which makes communication easy but synchronization risky. Processes isolate memory by default, which is safer in some ways but makes data sharing more explicit.

That means you should also choose based on how the workers need to communicate, not only on raw speed.

A Practical Rule of Thumb

Use this decision pattern:

  • waiting on network or disk: threads or async I/O
  • pure CPU-heavy Python work: processes
  • native code libraries that release the GIL: threads may still help

That last point matters because libraries such as NumPy may release the GIL during heavy native work, which changes the performance story.

Common Pitfalls

  • Expecting Python threads to speed up CPU-bound pure-Python loops in CPython.
  • Reaching for multiprocessing without considering serialization and process-start overhead.
  • Assuming threads and processes are interchangeable concurrency tools.
  • Forgetting the if __name__ == "__main__": guard when using multiprocessing on platforms that require it.
  • Benchmarking a tiny task and then drawing conclusions that do not hold for real workloads.

Summary

  • The GIL is the main reason Python threads usually do not speed up CPU-bound pure-Python code.
  • Threads are still very good for I/O-bound concurrency.
  • Multiprocessing enables real parallel CPU execution across cores.
  • Processes cost more overhead, so they are best when the CPU work is substantial.
  • Choose threads, processes, or async tools based on the workload, not by habit.

Course illustration
Course illustration

All Rights Reserved.