Skip to content

threading vs multiprocessing

Both are standard-library modules for running code concurrently, but with fundamentally different execution models: threading runs multiple threads inside one process, sharing memory directly but limited by the GIL to one thread executing Python bytecode at a time. multiprocessing runs multiple separate OS processes, each with its own interpreter and memory space β€” no GIL contention, but no free memory sharing either.

Without either, concurrent-looking work has to be simulated with manual interleaving or just run sequentially β€” no real overlap during I/O waits, no real parallelism for CPU work:

def download(url):
... # blocks for however long the network call takes
for url in urls:
download(url) # entirely sequential β€” each waits for the previous to finish
# threading β€” great for I/O-bound work (waiting on network/disk)
import threading
def download(url, results, i):
results[i] = fetch(url) # blocks on I/O, but releases the GIL while waiting
results = [None] * len(urls)
threads = [threading.Thread(target=download, args=(u, results, i))
for i, u in enumerate(urls)]
for t in threads: t.start()
for t in threads: t.join()
# all downloads overlap while waiting on the network β€” real speedup
# multiprocessing β€” needed for CPU-bound work (actual computation)
from multiprocessing import Pool
def crunch_numbers(n):
return sum(i * i for i in range(n))
if __name__ == "__main__":
with Pool(processes=4) as pool:
results = pool.map(crunch_numbers, [10_000_000] * 4)
# genuinely runs on 4 cores in parallel β€” each process has its own GIL
  • Match the tool to the bottleneck: threading for I/O-bound (network calls, file I/O, database queries β€” mostly waiting); multiprocessing for CPU-bound (number crunching, image processing, data transformation β€” mostly computing).
  • threading is lighter weight: threads share memory directly and are cheaper to create than processes, so it’s the better default when the GIL isn’t actually the bottleneck (i.e., I/O-bound work).
  • multiprocessing gets real parallelism: each process has its own GIL, so CPU-bound work genuinely spreads across cores β€” something threading structurally cannot do in CPython.
  • Sharing data between threads is direct (same memory) but requires locks (threading.Lock) to avoid race conditions on shared mutable state; sharing data between processes requires explicit serialization (multiprocessing.Queue, Pipe, or shared-memory objects) since they don’t share an address space.
  • multiprocessing has real overhead: process startup is slower than thread startup, and passing data between processes means pickling/unpickling it β€” for small, fast tasks that overhead can outweigh the parallelism gained.
  • Both are increasingly complemented (not replaced) by asyncio for I/O-bound work with very high concurrency (thousands of simultaneous connections) β€” see async/await β€” since one thread running an event loop can juggle far more concurrent I/O than one thread per connection would allow.
  • On some platforms, multiprocessing’s default start method requires the if __name__ == "__main__": guard shown above, or child processes can end up re-importing and re-running the parent script.
  1. Would you reach for threading or multiprocessing to speed up downloading 100 files over the network?

    Answer`threading` β€” it's I/O-bound (mostly waiting), and threads release the GIL during I/O waits, so they genuinely overlap.
  2. Would you reach for threading or multiprocessing to speed up resizing 100 large images?

    Answer`multiprocessing` β€” that's CPU-bound work; threads can't run Python bytecode in parallel due to the GIL, so only separate processes get real parallelism.
  3. Why is sharing data between processes harder than between threads?

    AnswerThreads share the same memory space directly; processes don't, so data has to be explicitly serialized and sent through a `Queue`/`Pipe` or a shared-memory construct.