A Read-Append-Write (RAW) lock
A RAW lock allows multiple concurrent readers, at most one appender,
which is allowed to run concurrently with the readers, and at most
one writer, which has exclusive access to the lock.
The following table summarises which roles are allowed to concurrently
access the RAW lock:
| Reader | Appender | Writer |
|---|
Reader | V | V | X |
Appender | ░░░░░░░░ | X | X |
Writer | ░░░░░░░░ | ░░░░░░░░░░ | X |
It is important to realise that a RAW lock is intended to control access to
a piece of in-memory state that should remain in sync with some other state
that can only be modified using side-effects, e.g., the file system. If,
for example, you're only maintaining a counter shared by threads, then
simply use a TVar or an MVar.
Example use case: log files
A RAW lock is useful, for example, to maintain an in-memory index of log
files stored on disk.
To read data from a log file, you need "read" access to the index to
find out the file and offset where the requested piece of data is stored.
While holding the RAW lock as a reader, you can perform the IO operation
to read the data from the right log file. This can safely happen
concurrently with other read operations.
To append data to the current log file, you need "append" access to the
index so you can append an entry to the index and even to add a new log
file to the index when necessary. While holding the RAW lock as an
appender, you can perform the IO operation to append the piece of data to
the current log file and, if necessary start a new log file. Only one
append can happen concurrently. However, reads can safely happen
concurrently with appends. Note that the in-memory index is only updated
after writing to disk.
To remove the oldest log files, you need "write" access to the index,
so you can remove files from the index. While holding the RAW lock as a
writer, you can perform the IO operations to delete the oldest log files.
No other operations can run concurrently with this operation: concurrent
reads might try to read from deleted files and a concurrent append could
try to append to a deleted file.
Analogy: Chicken coop
Think of readers as chickens, the appender as the rooster, and the writer
as the fox. All of them want access to the chicken coop, i.e., the state
protected by the RAW lock.
We can allow multiple chickens (readers) together in the chicken coop, they
get along (reasonably) fine. We can also let one rooster (appender) in, but
not more than one, otherwise he would start fighting with the other rooster
(conflict with the other appender). We can only let the fox in when all
chickens and the rooster (if present) have left the chicken coop, otherwise
the fox would eat them (conflict with the appender and invalidate the
results of readers, e.g, closing resources readers try to access).
Usage
To use the lock, use any of the three following operations:
If the standard bracketing the above three operations use doesn't suffice,
use the following three acquire-release pairs:
NOTE: an acquire must be followed by the corresponding release,
otherwise the correctness of the lock is not guaranteed and a dead-lock can
happen.
NOTE: nested locking of the same lock is not allowed, as you might be
blocked on yourself.
Notes
We do not guarantee fairness. Once the lock is released, all waiting
actors will race for the access.
The state st is always evaluated to WHNF and is subject to the
NoThunks check when enabled.
All public functions are exception-safe.