← Back to Python series
🚀
Advanced
Thread · Process · Lock · Queue · concurrent.futures

Week 4 — Threading & Multiprocessing

Write concurrent programs with threading and multiprocessing. Understand the GIL, synchronize shared state with Lock and Queue, and use concurrent.futures for a clean high-level API.

threadingmultiprocessingGILLockconcurrent.futures
Duration
3 hours
Level
📊 Advanced
Prerequisite
🎯 Intermediate Weeks 1–3
OUTCOME
Build a parallel image downloader and a multi-process number cruncher

What you'll learn

  • 1Explain the GIL and when threading vs multiprocessing is appropriate
  • 2Create and join threads with threading.Thread
  • 3Synchronize shared state with Lock
  • 4Use Queue for producer-consumer patterns
  • 5Speed up CPU-bound tasks with ProcessPoolExecutor

1. Threading

The GIL allows only one thread to execute Python bytecode at a time. Threading is best for I/O-bound tasks (network, disk).

python
import threading, time

def fetch(url, results, idx):
    time.sleep(0.5)   # simulate network I/O
    results[idx] = f"data from {url}"

urls = ["http://a.com", "http://b.com", "http://c.com"]
results = [None] * len(urls)
threads = [threading.Thread(target=fetch, args=(u, results, i)) for i, u in enumerate(urls)]
for t in threads: t.start()
for t in threads: t.join()
print(results)

2. Lock & Queue

python
import threading
from queue import Queue

lock = threading.Lock()
counter = 0

def increment(n):
    global counter
    for _ in range(n):
        with lock:
            counter += 1

t1 = threading.Thread(target=increment, args=(10000,))
t2 = threading.Thread(target=increment, args=(10000,))
t1.start(); t2.start()
t1.join(); t2.join()
print(counter)   # always 20000 with lock

3. concurrent.futures

python
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time

# I/O-bound: use ThreadPoolExecutor
def download(url):
    time.sleep(0.5)   # simulate
    return f"result:{url}"

with ThreadPoolExecutor(max_workers=4) as executor:
    futures = [executor.submit(download, f"http://site{i}.com") for i in range(8)]
    results = [f.result() for f in futures]
print(len(results), "downloads done")

# CPU-bound: use ProcessPoolExecutor
def is_prime(n):
    if n < 2: return False
    return all(n % i for i in range(2, int(n**0.5)+1))

with ProcessPoolExecutor() as executor:
    primes = list(executor.map(is_prime, range(1000)))

💻 Examples

Run these examples and check the output yourself.

01_thread_pool.pyParallel URL checker using ThreadPoolExecutor
CODE
from concurrent.futures import ThreadPoolExecutor, as_completed
import urllib.request, time

URLs = [
    "https://www.python.org",
    "https://docs.python.org",
    "https://pypi.org",
]

def check_url(url):
    try:
        start = time.perf_counter()
        urllib.request.urlopen(url, timeout=5)
        return url, True, time.perf_counter() - start
    except Exception as e:
        return url, False, 0

with ThreadPoolExecutor(max_workers=len(URLs)) as ex:
    futures = {ex.submit(check_url, u): u for u in URLs}
    for future in as_completed(futures):
        url, ok, elapsed = future.result()
        status = "OK" if ok else "FAIL"
        print(f"[{status}] {url}  ({elapsed:.2f}s)")

📝 Exercises

Try them yourself first, then open the solution to compare.

Exercise 1

Producer-Consumer Queue

Goal: Implement a producer-consumer pattern with threading.Queue.

Requirements
  • 1 producer thread generates 20 work items
  • 4 consumer threads process items (simulate with sleep)
  • Use Queue to safely pass items
  • Print when each item is produced and consumed
Toggle solution
SOLUTION
import threading, queue, time, random

work_q = queue.Queue()

def producer():
    for i in range(20):
        item = f"item-{i:02d}"
        work_q.put(item)
        print(f"  Produced: {item}")
        time.sleep(random.uniform(0.05, 0.1))
    for _ in range(4):   # sentinel values
        work_q.put(None)

def consumer(cid):
    while True:
        item = work_q.get()
        if item is None:
            work_q.task_done()
            break
        time.sleep(random.uniform(0.1, 0.3))
        print(f"  Consumer-{cid}: processed {item}")
        work_q.task_done()

threads = [threading.Thread(target=producer)]
threads += [threading.Thread(target=consumer, args=(i,)) for i in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print("All done")
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗