SoFunction
Updated on 2024-11-16

Sample code to implement read/write locks with Python

set in motion

Python's multithreading model does not provide read/write locks. Read/write locks are more applicable than simple mutex locks, which can be used by more than one thread at the same time in read mode, but can only be used by one thread in write mode.

In layman's terms, this means that when there is no write lock, you can add a read lock and any thread can be added at the same time; whereas a write lock can only have one thread, and must be added when there is no read lock.

simple implementation

import threading

class RWlock(object):
  def __init__(self):
    self._lock = ()
    self._extra = ()
    self.read_num = 0

  def read_acquire(self):
    with self._extra:
      self.read_num += 1
      if self.read_num == 1:
        self._lock.acquire()

  def read_release(self):
    with self._extra:
      self.read_num -= 1
      if self.read_num == 0:
        self._lock.release()

  def write_acquire(self):
    self._lock.acquire()

  def write_release(self):
    self._lock.release()

This is a simple implementation of a read/write lock. self.read_num is used to hold the number of threads that have acquired the read lock, and this attribute belongs to the critical zone, which is also subject to locking, so an additional lock, self._extra, is needed here to protect the internal data.

But this lock is unfair. Ideally, threads should have the same chance of acquiring the lock, regardless of whether the thread is reading or writing. As you can see from the code above, a read request sets self.read_num += 1 immediately, regardless of whether the lock is acquired or not, while a write request has to wait for read_num to be 0 in order to acquire the lock.

So this one creates a situation where write access can be gained only if the lock is not occupied or if there are no read requests. We should find a way to avoid read mode locks being occupied for a long time.

Priority of read and write locks

There are also read-first and write-first locks. The above code is read-first.

If you want to change to write-first, then switch to record the reference count of the write thread, read and write at the same time when the competition, you can let the write thread to increase the write count, so that the read thread can make the read thread read the lock has not been obtained, because the read thread to determine the reference count of the write, if not 0, then wait for it to be 0, and then read. This part of the code is not listed.

But this is clearly not flexible enough. We don't need two similar read/write lock classes. We want to refactor our code to make it more powerful.

revised

In order to be able to satisfy a read/write lock with a customized priority, the number of waiting read/write threads should be kept track of, and two conditions should be used to handle the notification of which side has priority. The count reference can be extended with semantics: positive number: indicates the number of threads in a read operation, negative number: indicates the number of threads in a write operation (up to -1)

In the acquisition of the read operation, first and then determine when there is a waiting write thread, there is no, read the operation, there is, then waiting to read the count plus 1 after waiting for Condition notification; waiting to read the count minus 1, the count references plus 1, continue to read the operation, if the conditions do not hold, the cycle of waiting;

When acquiring a write operation, if the lock is not occupied, the reference count is decremented by 1. If it is occupied, the number of threads waiting to write is increased by 1, and the notification of the write condition Condition is waited for.

The release is the same for both read and write modes, and the corresponding Condition needs to be notified based on the judgment:

class RWLock(object):
  def __init__(self):
     = ()
     = ()
     = ()
    self.read_waiter = 0  # of threads waiting to acquire a read lock
    self.write_waiter = 0  # of threads waiting to acquire a write lock
     = 0     # Positive number: indicates number of threads in read operation Negative number: indicates number of threads in write operation (up to -1)
     = []    # Collection of thread ids being manipulated
    self.write_first = True # Write-first by default, False for read-first

  def write_acquire(self, blocking=True):
    # Acquire a write lock only if
    me = threading.get_ident()
    with :
      while not self._write_acquire(me):
        if not blocking:
          return False
        self.write_waiter += 1
        ()
        self.write_waiter -= 1
    return True

  def _write_acquire(self, me):
    # Acquire a write lock only if the lock is unoccupied, or if the current thread is already occupied.
    if  == 0 or ( < 0 and me in ):
       -= 1
      (me)
      return True
    if  > 0 and me in :
      raise RuntimeError('cannot recursively wrlock a rdlocked lock')
    return False

  def read_acquire(self, blocking=True):
    me = threading.get_ident()
    with :
      while not self._read_acquire(me):
        if not blocking:
          return False
        self.read_waiter += 1
        ()
        self.read_waiter -= 1
    return True

  def _read_acquire(self, me):
    if  < 0:
      # If the lock is occupied by a write lock
      return False

    if not self.write_waiter:
      ok = True
    else:
      ok = me in 
    if ok or not self.write_first:
       += 1
      (me)
      return True
    return False

  def unlock(self):
    me = threading.get_ident()
    with :
      try:
        (me)
      except ValueError:
        raise RuntimeError('cannot release un-acquired lock')

      if  > 0:
         -= 1
      else:
         += 1
      if not :
        if self.write_waiter and self.write_first:  # If a write operation is pending (default write priority)
          ()
        elif self.read_waiter:
          .notify_all()
        elif self.write_waiter:
          ()

  read_release = unlock
  write_release = unlock

This is the whole content of this article.