# nobodd.locks

This module implements an SREW lock with re-entrancy and optional non-blocking behaviours. This is used in the `FatFileSystem` and `FatPath` implementations to safely manage multi-threaded access to the underlying file-system.

## Classes

class nobodd.locks.RWLock

  Synchronization object used in a solution of so-called second readers-writers problem.

  In this problem, many readers can simultaneously access a share, and a writer has an exclusive access to this share. Additionally, the following constraints should be met:

  1. no reader should be kept waiting if the share is currently opened for reading unless a writer is also waiting for the share,

  2. no writer should be kept waiting for the share longer than absolutely necessary.

  The implementation is based on [1], secs. 4.2.2, 4.2.6 with modifications for a more “Pythonic” style, and re-entrancy. The class provides two objects, `read` and `write` which each support the context manager protocol along with the typical `acquire` and `release` methods.

  Unlike the implementation in the book, both `read` and `write` are re-entrant (in the book version, `read` is re-entrant by virtue of it being shared, but `write` is not). Furthermore, upgrade is permitted from read lock to write lock, provided all locks are subsequently released in the order they were obtained. In other words, a thread may obtain a read lock, then implicitly “upgrade” to a write lock, provided it subsequently releases the write lock, then the original read lock.

  read

    The sub-object providing access to read locks. This object supports the context manager protocol, and the typical `acquire` and `release` methods (see `Lock.acquire()` and `Lock.release()`).

    The lock obtained via this object is re-entrant and shared (many threads may simultaneously share the read lock).

  write

    The sub-object providing access to write locks. This object supports the context manager protocol, and the typical `acquire` and `release` methods (see `Lock.acquire()` and `Lock.release()`).

    The lock obtained via this object is re-entrant but exclusive (no other thread may hold a read or write lock while this is held). A thread may upgrade from a read lock to a write lock, but the lock is not held continuously during upgrade: the read lock is released before the upgraded write lock is obtained. Bear in mind another write lock may be obtained by another thread before this thread obtains the upgraded write lock.

class nobodd.locks.RWLockState

  State of a thread in `RWLock`. This tracks the number of re-entrant calls a thread has made.

  Consider a hypothetical thread performing a series of operations. The values that its RWLockState move through will be as follows:

  Operation | read | write | ignored | Notes
  --- | --- | --- | --- | ---
  | 0 | 0 | 0 | start
  read.acquire | 1 | 0 | 0 |
  read.acquire | 2 | 0 | 0 |
  read.acquire | 3 | 0 | 0 |
  write.acquire | 3 | 1 | 0 | upgrade
  write.acquire | 3 | 2 | 0 |
  read.acquire | 3 | 2 | 1 | ignore
  read.acquire | 3 | 2 | 2 |
  read.release | 3 | 2 | 1 |
  read.release | 3 | 2 | 0 |
  write.release | 3 | 1 | 0 |
  write.release | 3 | 0 | 0 | downgrade
  read.release | 2 | 0 | 0 |
  read.release | 1 | 0 | 0 |
  read.release | 0 | 0 | 0 |

  upgrade

    At this point this thread holds three re-entrant acquisitions of the read lock, but has requested a write lock (effectively requesting to upgrade its lock). What this does is release all the read locks held (actually one lightswitch release), and start a write lock acquisition

  ignore

    If a thread holds any write locks (whether through upgrade, or because it originally grabbed a write lock) all attempted acquisitions of a read lock are ignored (but counted)

  downgrade

    Here a thread has released its last write lock while in an upgraded state. The implementation will release the write lock then re-acquire the number of read-locks specified (actually one lightswitch acquisition)

class nobodd.locks.LightSwitch(lock)

  An auxiliary “light switch”-like object. The first thread to acquire the switch also acquires the lock associated with the switch. The last thread to release the switch also releases the lock associated with the switch.

  If the first and last threads can differ, you must use a primitive `threading.Lock` or `threading.Semaphore` (initialized to 1) as the value of the lock parameter. If the first and last threads will always be the same, you may use a re-entrant `threading.RLock`.

  This implementation is based on [1], sec. 4.2.2 but with additions to permit non-blocking execution or timeouts in blocking mode, and operation as a context manager.

  [1] (1,2)

  A.B. Downey: “The little book of semaphores”, Version 2.2.1, 2016 https://greenteapress.com/wp/semaphores/

## Support functions

nobodd.locks.remaining(timeout, start)

  Calculate the amount of timeout remaining if the routine started at start (measured by `time.monotonic()`).

  If timeout is -1 (meaning infinite timeout), this function always returns -1. Otherwise, the time remaining is calculated and clamped to 0.