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
| Term | Definition |
|---|---|
| GIL | Global Interpreter Lock — prevents true thread-level parallelism in CPython |
| Thread | Lightweight unit of execution sharing memory with other threads |
| Process | Independent unit with its own memory space |
| Race condition | Bug when two threads modify shared data simultaneously |
| Lock | Mutex preventing concurrent access to shared state |
ThreadPoolExecutor | High-level thread pool (I/O-bound work) |
ProcessPoolExecutor | High-level process pool (CPU-bound work) |
Summary
- The GIL limits threading for CPU-bound tasks — use
multiprocessinginstead threadingis effective for I/O-bound tasks (network calls, file I/O)concurrent.futuresprovidesThreadPoolExecutorandProcessPoolExecutorwith a clean API- Protect shared state with
threading.Lock()to prevent race conditions - Rule: I/O-bound → threads; CPU-bound → processes
📄️ 09.1 - Generators & Iterators
Write memory-efficient code with Python iterators, generator functions, generator expressions, and itertools
📄️ 09.2 - Threading & Multiprocessing
Run code concurrently with threading (I/O-bound) and multiprocessing (CPU-bound), understand the GIL, and use concurrent.futures
📄️ 09.3 - Async/Await
Write non-blocking Python code with async/await, coroutines, asyncio.gather, and aiohttp for async HTTP
📄️ Lab - Module 09
Build a concurrent URL checker using asyncio, generators, and itertools
📄️ Quiz - Module 09
30 questions on generators, iterators, threading, multiprocessing, and asyncio