Python - Concurrency & Parallelism
Overview
Estimated time: 45–60 minutes
Understand the GIL, when to use threads vs processes, and how to write simple asyncio programs. Learn async pitfalls and safe patterns.
Learning Objectives
- Explain when threading helps (I/O-bound) vs multiprocessing (CPU-bound).
- Write a basic asyncio task with timeouts/cancellation.
- Apply safe patterns: avoid blocking the event loop; use run_in_executor for CPU-bound.
Prerequisites
Threading (I/O-bound)
import threading, time
def worker(n: int):
time.sleep(0.1)
print(f"done {n}")
threads = [threading.Thread(target=worker, args=(i,)) for i in range(3)]
for t in threads: t.start()
for t in threads: t.join()
Multiprocessing (CPU-bound)
from multiprocessing import Pool
def square(x): return x*x
with Pool() as p:
print(p.map(square, [1,2,3,4]))
Expected Output: [1, 4, 9, 16]
asyncio (3.7+)
import asyncio
async def task(n):
await asyncio.sleep(0.1)
return n
async def main():
try:
res = await asyncio.wait_for(asyncio.gather(*(task(i) for i in range(3))), timeout=1)
print(res)
except asyncio.TimeoutError:
print("timeout")
asyncio.run(main())
Expected Output: [0, 1, 2]
Common Pitfalls
- Blocking the event loop with synchronous I/O or CPU work.
- Sharing mutable state between threads without locks; prefer queues.
Checks for Understanding
- When should you reach for threads vs processes?
- How do you avoid blocking the event loop for CPU-bound tasks?
Show answers
- Threads for I/O-bound; processes for CPU-bound work.
- Use run_in_executor or move work to a process.
Exercises
- Use ThreadPoolExecutor to fetch multiple URLs concurrently (I/O-bound demo).
- Create an asyncio program with two tasks and a timeout; handle cancellation gracefully.