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

  1. When should you reach for threads vs processes?
  2. How do you avoid blocking the event loop for CPU-bound tasks?
Show answers
  1. Threads for I/O-bound; processes for CPU-bound work.
  2. Use run_in_executor or move work to a process.

Exercises

  1. Use ThreadPoolExecutor to fetch multiple URLs concurrently (I/O-bound demo).
  2. Create an asyncio program with two tasks and a timeout; handle cancellation gracefully.