Skip to main content

Multiprocessing vs. Threading & ThreadPoolExecutor

When choosing between threading and multiprocessing in Python, remember the ultimate rule of hardware:

  • Threading: Perfect for Network I/O & API calls where threads spend 95% of their time waiting on web sockets.
  • Multiprocessing: Perfect for Heavy CPU math & Image processing because it spawns separate Python processes, each with its own independent Python interpreter and CPU core, completely bypassing the GIL.

1. Modern Concurrency: concurrent.futures​

Modern Python engineers rarely create threads manually using threading.Thread loops.

Instead, they use ThreadPoolExecutor from Python's standard concurrent.futures library, which automatically manages a pool of background worker threads for you:

from concurrent.futures import ThreadPoolExecutor
import time

def fetch_stock_price(symbol):
print(f"Fetching {symbol}...")
time.sleep(1) # Simulate network API latency
return f"{symbol}: ₹2,450"

stock_symbols = ["TCS", "INFY", "RELIANCE", "HDFCBANK", "WIPRO"]

# Create a pool of 5 worker threads
with ThreadPoolExecutor(max_workers=5) as executor:
# .map() distributes the list across threads and preserves result order
results = executor.map(fetch_stock_price, stock_symbols)

for res in results:
print(res)
# All 5 stocks fetched concurrently in ~1.0 second instead of 5.0 seconds!

2. Using multiprocessing for CPU-Bound Work​

For computationally intensive math (like hashing 10,000 passwords or resizing images), ProcessPoolExecutor utilizes all CPU cores in your laptop:

from concurrent.futures import ProcessPoolExecutor
import time

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

if __name__ == '__main__':
data_loads = [10000000, 10000000, 10000000, 10000000]

start = time.perf_counter()
with ProcessPoolExecutor() as executor:
results = list(executor.map(compute_heavy_factors, data_loads))

print(f"All 4 CPU cores finished in: {time.perf_counter() - start:.2f}s")
if __name__ == '__main__': Guard

When using multiprocessing on Windows or macOS, you must always wrap your entrypoint inside if __name__ == '__main__': to prevent child processes from infinitely re-importing the main script.


Quick Summary​

Featurethreading / ThreadPoolExecutormultiprocessing / ProcessPoolExecutor
Best Used ForNetwork requests, Web scraping, APIs, DB reads (I/O-Bound)Heavy math, Video encoding, ML training (CPU-Bound)
Memory FootprintShared RAM memory (Very low ~8 KB per thread)Separate RAM memory per process (~20 MB+)
GIL LimitationBound by GIL (Single core for pure Python code)Bypasses GIL completely (Utilizes all CPU cores)
IPC CommunicationDirect memory sharingRequires Pipes, Queues, or Pickling

What's Next?​

Now let's learn how to write automated test suites, verify edge cases, and ensure production software quality using Module 24: Unit Testing with unittest & pytest!