Threads and locks
Share memory safely and wait for work to finish
Threads
Threads are independently scheduled execution paths within a process. They share Python objects and can overlap blocking I/O, but their operations can interleave.
Create Thread(target=function, args=(value,)), call start(), then join() to wait for completion. Calling run() directly stays in the current thread. Start workers before joining them to allow overlap; a thread object can start only once.
from threading import Lock, Thread
numbers = []
lock = Lock()
def record(value):
with lock:
numbers.append(value)
threads = [Thread(target=record, args=(n,)) for n in (1, 2)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print(sorted(numbers)) # [1, 2]Joining ensures both workers have finished. Their append order is unspecified; sorting stabilizes the displayed result, not execution order.
Critical sections
A lock protects an invariant across related operations. Keep a check and its update in one critical section:
from threading import Lock, Thread
remaining = 3
lock = Lock()
def reserve_two():
global remaining
with lock:
if remaining >= 2:
remaining -= 2
threads = [Thread(target=reserve_two) for _ in range(2)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print(remaining) # 1Only one reservation can pass. All readers and writers relying on this state must follow the same locking rule. In an application, an owning object can hold both state and lock.
Use with lock to release the lock after ordinary exceptions. Locking does not roll back earlier mutations if a later step fails. Validate first or define rollback, and return snapshots when callers should not mutate shared containers.
Gotchas
- In GIL-enabled CPython, the GIL limits simultaneous Python-bytecode execution; it does not make business operations atomic. Some libraries release it, and free-threaded builds have different behavior. Synchronize shared state explicitly.
- Avoid blocking I/O and unknown callbacks while holding a lock. Consistent lock acquisition order helps prevent deadlocks;
RLockpermits reentry but does not solve arbitrary lock-order cycles. join()waits for one worker, not every writer, and does not re-raise that worker's exception. Futures provide a result/failure channel.- More threads can increase overhead and overload dependencies. Daemon threads can stop abruptly, so important cleanup needs explicit ownership and joins.
This lesson requires native Python; the browser runtime does not provide ordinary operating-system threads.
Put it into practice
Implement the exercise in the workspace, then download it and run the tests with your local toolchain.