Aller au contenu principal

09.2 - Concurrency: Threading & Multiprocessing

Theory 30 min Advanced

The GIL — Global Interpreter Lock

The GIL is a mutex that allows only one thread to execute Python bytecode at a time in CPython.

┌────────────────────────────────────────────────────────┐
│ GIL Impact │
│ │
│ I/O-bound tasks (network, disk) │
│ ✅ Threading WORKS — GIL released during I/O │
│ │
│ CPU-bound tasks (math, data processing) │
│ ❌ Threading LIMITED — GIL prevents true parallelism │
│ ✅ Multiprocessing WORKS — separate GILs │
└────────────────────────────────────────────────────────┘

threading — For I/O-Bound Tasks

import threading
import time

def download(url, results, index):
print(f"Downloading {url}...")
time.sleep(1) # simulate I/O
results[index] = f"Data from {url}"

urls = ["api.example.com/1", "api.example.com/2", "api.example.com/3"]
results = [None] * len(urls)
threads = []

for i, url in enumerate(urls):
t = threading.Thread(target=download, args=(url, results, i))
threads.append(t)
t.start()

for t in threads:
t.join() # wait for all to finish

print(results) # all 3 results after ~1s (not ~3s!)

concurrent.futures — High-Level API

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

# Thread pool (I/O-bound)
with ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(fetch_url, url) for url in urls]
results = [f.result() for f in futures]

# Map style (like built-in map)
with ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(fetch_url, urls))

# Process pool (CPU-bound)
def cpu_intensive(n):
return sum(i**2 for i in range(n))

with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(cpu_intensive, [10**6]*4))

multiprocessing — For CPU-Bound Tasks

from multiprocessing import Pool, Process

def square(x):
return x ** 2

# Process pool
with Pool(processes=4) as pool:
results = pool.map(square, range(100))

# Shared state
from multiprocessing import Value, Array

counter = Value('i', 0)
with counter.get_lock():
counter.value += 1

Threading Safety

import threading

# Thread-safe counter using Lock
class Counter:
def __init__(self):
self._count = 0
self._lock = threading.Lock()

def increment(self):
with self._lock: # acquire and release automatically
self._count += 1

@property
def value(self):
return self._count

Key Vocabulary

TermDefinition
GILGlobal Interpreter Lock — prevents true thread-level parallelism in CPython
ThreadLightweight unit of execution sharing memory with other threads
ProcessIndependent unit with its own memory space
Race conditionBug when two threads modify shared data simultaneously
LockMutex preventing concurrent access to shared state
ThreadPoolExecutorHigh-level thread pool (I/O-bound work)
ProcessPoolExecutorHigh-level process pool (CPU-bound work)

Summary

  • The GIL limits threading for CPU-bound tasks — use multiprocessing instead
  • threading is effective for I/O-bound tasks (network calls, file I/O)
  • concurrent.futures provides ThreadPoolExecutor and ProcessPoolExecutor with a clean API
  • Protect shared state with threading.Lock() to prevent race conditions
  • Rule: I/O-bound → threads; CPU-bound → processes