Semaphore, Bounded Semaphore and RWLock
Beyond the plain lock, Python provides semaphores to limit how many threads may enter a region at once, and reader-writer locks (not in the standard library) to allow many concurrent readers but exclusive writers. These two primitives cover the classic "resource pool" and "cached data" patterns.
Semaphore
A semaphore starts with a counter n. Each acquire() decrements it, each release() increments it; when the counter reaches zero, further acquires block. The standard use is capping concurrency - for example, at most 5 workers hitting an API that rate-limits at 5 requests per second:
import threading
import time
sem = threading.Semaphore(5)
def fetch(url):
with sem: # max 5 in flight
print("fetching", url)
time.sleep(1)
threads = [threading.Thread(target=fetch, args=(f"/page/{i}",)) for i in range(20)]
[t.start() for t in threads]
[t.join() for t in threads]
Bounded Semaphore
A BoundedSemaphore(n) additionally raises ValueError if you release more times than you acquired. That turns a logic bug (a code path that releases without acquiring) into an immediate error instead of a semaphore that silently admits more threads than the limit:
sem = threading.BoundedSemaphore(1)
sem.acquire()
sem.release()
sem.release() # ValueError: release without acquire
Reader-Writer Lock
The standard library has no RWLock, but the pattern is simple with a lock and two conditions: any number of readers proceed together, while a writer needs the lock alone. A compact correct version:
import threading
class RWLock:
def __init__(self):
self._lock = threading.Lock()
self._readers = 0
def acquire_read(self):
with self._lock:
self._readers += 1
def release_read(self):
with self._lock:
self._readers -= 1
def acquire_write(self):
self._lock.acquire()
def release_write(self):
self._lock.release()
Readers count themselves under the lock so a writer arriving mid-read waits for the current cohort to finish. In practice, use this shape for in-memory caches and config tables that are read far more often than they change.
Summary
Use a semaphore to bound the number of concurrent users of a scarce resource, a bounded semaphore when release bugs should fail loudly, and a reader-writer pattern when reads vastly outnumber writes and you want parallel reads.