Level: Advanced | Prerequisite: Week 3
Learning Goals
- Distinguish between I/O-bound and CPU-bound tasks
- Accelerate I/O tasks with
ThreadPoolExecutor - Accelerate CPU tasks with
ProcessPoolExecutor - Understand the meaning of the GIL
- Synchronize using
threading.Lock
1. GIL (Global Interpreter Lock)
CPython executes only one thread of Python bytecode at a time → CPU-bound tasks cannot be accelerated with threading. Effective for I/O operations (network, disk).
| Type of Task | Tool |
|---|---|
| I/O-bound (network, file) | threading, asyncio |
| CPU-bound (computation, encoding) | multiprocessing |
2. ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor
import requests
urls = ["https://example.com"] * 10
def fetch(url):
return len(requests.get(url).text)
with ThreadPoolExecutor(max_workers=5) as ex:
results = list(ex.map(fetch, urls))
About 5 times faster compared to sequential execution (other tasks run during network wait).
3. ProcessPoolExecutor
from concurrent.futures import ProcessPoolExecutor
def heavy(n):
return sum(i * i for i in range(n))
if __name__ == "__main__": #Windows is required
with ProcessPoolExecutor() as ex:
print(list(ex.map(heavy, [1_000_000] * 4)))
4. submit / as_completed
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor() as ex:
futures = [ex.submit(fetch, url) for url in urls]
for fut in as_completed(futures):
print(fut.result())
5. threading.Lock
Prevents data corruption when multiple threads modify the same resource.
import threading
counter = 0
lock = threading.Lock()
def inc():
global counter
for _ in range(100_000):
with lock:
counter += 1
ts = [threading.Thread(target=inc) for _ in range(4)]
for t in ts: t.start()
for t in ts: t.join()
print(counter) # 400000
Without a Lock, results are small or variable.
Key Examples (examples/)
| File | Content |
|---|---|
01_thread_pool.py |
ThreadPoolExecutor (I/O) |
02_process_pool.py |
ProcessPoolExecutor (CPU) |
03_lock.py |
threading.Lock |
04_compare.py |
Sequential vs Thread Time Comparison |
⚠️ Common Mistakes
- Using thread for CPU tasks — May become slower due to GIL
- Missing
__main__guard (Windows + multiprocessing) — Infinite loop - Missing Lock on shared resource — Non-deterministic results
- Not retrieving Future results — Exceptions are hidden
❓ FAQ
Q1. Compared to asyncio? A. asyncio is cooperative in a single thread. threading is scheduled by the OS. If I/O is light, use asyncio; if library compatibility is important, use threading.
Q2. What are the disadvantages of multiprocessing? A. High cost to start a process. Small tasks may actually be slower. Object serialization (pickle) is required.
📝 Exercises (exercises/)
exercise1.md— Simultaneous downloading of multiple URLs (time comparison)exercise2.md— Accelerating CPU tasks with process poolexercise3.md— Comparison before and after applying shared counter + Lock
Next Week
Week 5. Concurrency (2) - asyncio