Module 1 · Foundations that hold everything up

Lesson 03 — Concurrency and asynchrony

Threads, processes, event loops, race conditions and deadlocks — with the TicketFlow purchase queue.

Published
In this lesson
  1. Exercise 1 — The counter race
  2. Exercise 2 — GIL: CPU vs I/O
  3. Exercise 3 — The check-then-act window, observed
  4. Exercise 4 — Prove I1 with real requests
  5. Exercise 5 — Deadlock in miniature (two locks, reverse order)
  6. Exercise 6 — Async or sync (reasoned)
  7. Exercise 7 — Your project's concurrency map
  8. Submit

Exercises 1-3 run in a loose script (race_lab.py); 4-6 go inside your TicketFlow. Do not look at solutions.md before submitting.

Setup:

bash
cd ~/dev/ticketflow && source .venv/bin/activate

Exercise 1 — The counter race

Copy this into race_lab.py and run it 5 times:

python
import threading

counter = 0

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

t1 = threading.Thread(target=increment, args=(100_000,))
t2 = threading.Thread(target=increment, args=(100_000,))
t1.start(); t2.start(); t1.join(); t2.join()
print(f"expected 200000, got {counter}")
  1. Write down the 5 results. Did any give exactly 200,000? Why do they vary between runs?
  2. Fix the race with threading.Lock() (lock the section that reads-adds-writes). Does it stop varying?
  3. What does performance become after the lock? (measure with time.perf_counter()).

Exercise 2 — GIL: CPU vs I/O

  1. CPU: two threads, each summing in a loop 10 million times. Measure the total time and compare with a single thread doing twice the work. Did it scale?
  2. I/O: two threads, each doing time.sleep(1) (simulates a DB call). Measure the total. Did it scale?
  3. Write in one line the conclusion you would give in an interview.

Exercise 3 — The check-then-act window, observed

  1. Write a small thread-based version of the pattern (no DB):
python
import threading, time

reservations = []
seat_taken = False

def reserve(user):
    global seat_taken
    if not seat_taken:              # CHECK
        time.sleep(0.001)           # ← the window (simulates the DB query)
        reservations.append(user)   # ACT
        seat_taken = True

threads = [threading.Thread(target=reserve, args=(u,)) for u in range(20)]
for t in threads: t.start()
for t in threads: t.join()
print(f"{len(reservations)} reservations for ONE seat")
  1. How many reservations get created? Tune the sleep (0.0001, 0.01): how does the disaster change?
  2. Fix the experiment two ways: (a) an in-memory lock; (b) turning the check+act into a single atomic operation (hint: what would the "single atomic operation" be in a real DB? name it, no need to implement it).

Exercise 4 — Prove I1 with real requests

On your real TicketFlow (the 00b constraint already protects it):

  1. Open two terminals and fire in parallel (same free seat, two users):
bash
# terminal 1
curl -s -X POST http://127.0.0.1:8000/api/events/1/reserve/ -H 'Content-Type: application/json' \
  -d '{"seat_id": 5, "user": "ana"}' & \
# terminal 2 (fire immediately after)
curl -s -X POST http://127.0.0.1:8000/api/events/1/reserve/ -H 'Content-Type: application/json' \
  -d '{"seat_id": 5, "user": "beto"}'
  1. What did each one answer? Any 500? Or did one get 201 and the other 409? If you saw a 500 with IntegrityError, that is an API design bug (Lesson 01): the constraint saved the data, but the view did not know how to translate the failure.
  2. Add the handling: catch IntegrityError in the view and respond 409 Conflict with a consistent body. Paste your diff.

Exercise 5 — Deadlock in miniature (two locks, reverse order)

  1. Run this "safe" version and confirm it finishes:
python
import threading

lock_a = threading.Lock()   # the seat's row
lock_b = threading.Lock()   # the user's row

def op1():   # always A → B
    with lock_a, lock_b:
        pass

threads = [threading.Thread(target=op1) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print("no deadlock")
  1. Create op2() that takes B → A and launch 1 thread of each in a loop. Does it hang? (if it doesn't hang within 10 s, force the window with a time.sleep(0.001) between acquisitions).
  2. Fix the deadlock by imposing a global order (all functions A → B). Does it still hang?

Exercise 6 — Async or sync (reasoned)

For each case, decide sync or async in Django and justify in 1-2 lines:

  1. GET /api/events/ — a listing that queries PostgreSQL.
  2. POST /api/payments/webhook/ — verifies the signature and enqueues the heavy work in Celery.
  3. GET /api/exchange-rates/ — calls 3 external currency APIs and aggregates the result.
  4. POST /api/reports/export/ — generates a 2 GB CSV in memory and compresses it.

Exercise 7 — Your project's concurrency map

Draw (text) the map from Lesson 6 with YOUR configuration: how many Gunicorn workers you would use for TicketFlow on a 2 vCPU / 4 GB VM, with which --threads, and which separate Celery queue. Justify each number.


Submit

Paste results and diffs in the chat. When corrected, Lesson 03 closes and Lesson 04 — Data structures and algorithms opens (with TicketFlow's seat cart as the example).