Credit: Sami Hangaslammi
“One-writer, many-readers” locks are a frequent necessity, and Python does not supply them directly. As usual, they’re not hard to program yourself, in terms of other synchronization primitives that Python does supply:
import threading class ReadWriteLock: """ A lock object that allows many simultaneous "read locks", but only one "write lock." """ def _ _init_ _(self): self._read_ready = threading.Condition(threading.Lock( )) self._readers = 0 def acquire_read(self): """ Acquire a read lock. Blocks only if a thread has acquired the write lock. """ self._read_ready.acquire( ) try: self._readers += 1 finally: self._read_ready.release( ) def release_read(self): """ Release a read lock. """ self._read_ready.acquire( ) try: self._readers -= 1 if not self._readers: self._read_ready.notifyAll( ) finally: self._read_ready.release( ) def acquire_write(self): """ Acquire a write lock. Blocks until there are no acquired read or write locks. """ self._read_ready.acquire( ) while self._readers > 0: self._read_ready.wait( ) def release_write(self): """ Release a write lock. """ self._read_ready.release( )
It is often convenient to allow unlimited read access to a resource
when it is not being modified and still keep write access exclusive.
threading module does not contain a
specific class for the job, the idiom is easy to implement using a
Condition object, and this recipe shows how you
can do that.
rw = ReadWriteLock( )
rw._readers counts the number of
readers who are currently in the read-write lock (initially zero).
The actual synchronization is performed by a
threading.Condition object (created at
_ _init_ _ around a new
Lock object and held in
methods increment and decrement the number of active readers. Of
course, this happens between
release calls to
bracketing is obviously necessary even to avoid race conditions
between different threads wanting to acquire or release a read lock.
But we also exploit
_read_ready for another
purpose, which is why
release_read also does a
notifyAll on it, if and when it notices it has
removed the last read lock.
notifyAll method of a
Condition object wakes up all threads (if any)
that are on a
wait condition on the object. In
this recipe, the only way a thread can get into such a wait is via
acquire_write method, when it finds there are
readers active after acquiring
wait call on the
object releases the underlying lock, so
release_read methods can execute, but reacquires
it again before waking up, so
safely keep checking whenever it wakes up, if it’s
finally in a no-readers-active situation. When that happens,
acquire_write returns to its caller, but keeps the
lock, so no other writer or reader can enter again, until the writer
release_write, which lets the lock go again.
Note that this recipe offers no guarantee against what is technically known as a starvation situation. In other words, there is no guarantee that a writer won’t be kept waiting indefinitely by a steady stream of readers arriving, even if no reader keeps its read lock for very long. If this is a problem in your specific application, you can avoid starvation by adding complications to ensure that new readers don’t enter their lock if they notice that a writer is waiting. However, in many cases, you can count on situations in which no readers are holding read locks, without special precautions to ensure that such situations occur. In such cases, this recipe is directly applicable, and besides eschewing complications, it avoids potentially penalizing reader performance by making several readers wait for one pending writer.