threading — Concurrency for I/O-Bound Work
IntermediateThreadPoolExecutor turns "call 50 slow APIs one by one" into "call them together" — and Lock protects the shared state that threads would otherwise corrupt.
Overview
Threads share one process and one memory space, which makes them cheap to start and trivially able to share data — and that sharing is exactly where the danger lives. The modern API is concurrent.futures.ThreadPoolExecutor: submit functions to a pool, collect results, never manage raw Thread objects for routine work. The classic bug is the race condition — two threads doing read-modify-write on the same variable, losing updates — fixed with threading.Lock or by passing data through queue.Queue instead of sharing it. In backend work this pattern appears everywhere: fan-out HTTP calls, parallel S3 downloads, background workers draining a queue.
ThreadPoolExecutor — the Only API You Need Daily
pool.map preserves input order; pool.submit + as_completed yields results as they finish. Ten 1-second network calls complete in about one second total.
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
def fetch(url):
time.sleep(1) # stands in for requests.get(url)
return f"{url} -> 200 OK"
urls = [f"https://api.razorpay.com/v1/payments/{i}" for i in range(10)]
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=10) as pool:
results = list(pool.map(fetch, urls)) # ordered results
print(f"{len(results)} calls in {time.perf_counter() - start:.1f}s") # ~1.0s
# submit + as_completed: handle each result the moment it arrives
with ThreadPoolExecutor(max_workers=10) as pool:
futures = {pool.submit(fetch, u): u for u in urls}
for fut in as_completed(futures):
print(futures[fut], "→", fut.result()) # fastest firstRace Conditions & Lock — the Bug Interviewers Love
counter += 1 is three bytecode steps (load, add, store); threads interleave between them and updates vanish. A with lock: block makes the sequence atomic. Better still: don't share — pass messages via queue.Queue, which is thread-safe.
import threading
counter = 0
lock = threading.Lock()
def deposit():
global counter
for _ in range(100_000):
with lock: # comment this out → lost updates
counter += 1 # load, +1, store — NOT atomic
threads = [threading.Thread(target=deposit) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # 400000 with the lock; a random smaller number without
# Prefer message passing over shared state:
import queue
q = queue.Queue() # thread-safe by design
q.put({"order_id": 101, "amount": 499})
item = q.get() # blocks until an item is available
q.task_done()Key Points to Remember
- 1Use ThreadPoolExecutor — map for ordered results, submit + as_completed for fastest-first
- 2Threads share memory: cheap to start, dangerous to mutate shared state
- 3x += 1 is not atomic — guard shared writes with threading.Lock (with lock:)
- 4queue.Queue is thread-safe; producer-consumer beats shared variables
Interview Questions
Sign in to ask AriaWrite code to download 100 URLs concurrently with a pool of 10 threads.
Two threads increment a counter 100k times each; the result is less than 200k. Explain and fix.
What is a deadlock? Show how acquiring two locks in different orders causes one.
Ask Aria about threading — Concurrency for I/O-Bound Work
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.