python
multiprocessing
threading
concurrency
parallelism

multiprocess or threading in python?

Master System Design with Codemia

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

Introduction

Python gives you two common ways to run work concurrently: threads and processes. Choosing the right one matters because the wrong model can make a program slower, harder to debug, or much more memory-hungry than necessary.

The core difference

Threads run inside one process and share the same memory space. Processes run as separate operating system processes, so they do not share memory by default.

That difference affects three things immediately:

  • how much setup overhead you pay
  • whether CPU-bound work can run in parallel
  • how safely tasks can share mutable state

In standard CPython, the Global Interpreter Lock means only one thread executes Python bytecode at a time. Because of that, threads are usually best for waiting on external systems, while processes are usually best for heavy computation.

When threading is the better choice

Threading shines for I/O-bound tasks such as:

  • calling web APIs
  • reading files
  • waiting on sockets
  • coordinating many slow external operations

While one thread waits, another can continue running. Here is a simple example:

python
1import threading
2import time
3
4def fetch(name, delay):
5    print(f"start {name}")
6    time.sleep(delay)
7    print(f"done {name}")
8
9threads = [
10    threading.Thread(target=fetch, args=("a", 1)),
11    threading.Thread(target=fetch, args=("b", 1)),
12]
13
14for thread in threads:
15    thread.start()
16
17for thread in threads:
18    thread.join()

This finishes in about one second rather than two because the waits overlap.

When multiprocessing is the better choice

Multiprocessing is the right default for CPU-bound work such as:

  • image processing
  • large numerical loops
  • parsing huge datasets
  • expensive search or optimization tasks

Because each worker is a separate process, the operating system can schedule them on different CPU cores. A small example:

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

This kind of workload benefits from real parallel execution, something ordinary Python threads cannot provide for pure Python CPU work.

How to decide quickly

A useful rule is simple:

  • if your task mostly waits, prefer threads
  • if your task mostly computes, prefer processes

There are exceptions. Native libraries such as NumPy may release the GIL, so threads can still help for some numerical workloads. Likewise, processes may be overkill for tiny tasks because starting them and serializing data costs time.

Data sharing is another major factor. Threads share objects directly, which is convenient but risky. Processes isolate state, which is safer but means data must be copied or passed through queues, pipes, or shared memory.

Communication patterns matter

With threads, you often coordinate with locks, queues, and events:

python
1import queue
2import threading
3
4jobs = queue.Queue()
5
6def worker():
7    while True:
8        item = jobs.get()
9        if item is None:
10            break
11        print(item * 2)
12        jobs.task_done()
13
14thread = threading.Thread(target=worker)
15thread.start()
16
17for number in [1, 2, 3]:
18    jobs.put(number)
19
20jobs.join()
21jobs.put(None)
22thread.join()

With processes, the safest model is similar: use message passing instead of shared mutable state whenever possible.

Common Pitfalls

One common mistake is using threads for CPU-bound loops and expecting all cores to be busy. In CPython, that usually does not happen because of the GIL.

Another mistake is forgetting the if __name__ == "__main__": guard when using multiprocessing. On platforms that use spawn semantics, missing that guard can recursively start new child processes.

Shared state is another source of bugs. Threads can corrupt data without proper locking, while processes can silently become slow if you pass large objects back and forth too often.

Finally, do not optimize too early. Concurrency adds complexity, so measure first and confirm whether the bottleneck is I/O or CPU time.

Summary

  • Threads are usually best for I/O-bound work.
  • Processes are usually best for CPU-bound work.
  • The GIL limits parallel execution of Python bytecode in threads.
  • Processes add startup and serialization overhead but allow true multi-core execution.
  • Choose a communication model early and prefer queues or message passing over ad hoc shared state.

Course illustration
Course illustration

All Rights Reserved.