Safe Haskell | None |
---|---|
Language | Haskell2010 |
A Read-Append-Write (RAW) lock
Intended for qualified import
Synopsis
- data RAWLock m st
- new :: ( IOLike m, NoThunks st) => st -> m ( RAWLock m st)
- poison :: ( IOLike m, Exception e, HasCallStack ) => RAWLock m st -> ( CallStack -> e) -> m ( Maybe st)
- read :: IOLike m => RAWLock m st -> STM m st
- withAppendAccess :: forall m st a. IOLike m => RAWLock m st -> (st -> m (st, a)) -> m a
- withReadAccess :: forall m st a. IOLike m => RAWLock m st -> (st -> m a) -> m a
- withWriteAccess :: forall m st a. IOLike m => RAWLock m st -> (st -> m (st, a)) -> m a
- unsafeAcquireAppendAccess :: IOLike m => RAWLock m st -> STM m st
- unsafeAcquireReadAccess :: IOLike m => RAWLock m st -> STM m st
- unsafeAcquireWriteAccess :: IOLike m => RAWLock m st -> m st
- unsafeReleaseAppendAccess :: IOLike m => RAWLock m st -> st -> STM m ()
- unsafeReleaseReadAccess :: IOLike m => RAWLock m st -> STM m ()
- unsafeReleaseWriteAccess :: IOLike m => RAWLock m st -> st -> m ()
Public API
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:
-
unsafeAcquireReadAccess
&unsafeReleaseReadAccess
-
unsafeAcquireAppendAccess
&unsafeReleaseAppendAccess
-
unsafeAcquireWriteAccess
&unsafeReleaseWriteAccess
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
- Only use a RAW lock when it is safe to concurrently read and append.
- We do not guarantee fairness for appenders and writers. They will race for access each time the RAW lock changes.
- When you have many writers and/or very frequent writes, readers and appenders will starve. You could say we have "unfairness", as writers win over readers and appenders. A RAW lock will not be the best fit in such a scenario.
-
When you have no writers and you only need a read-append lock, consider
using a
StrictMVar
instead. The "stale" state can be used by the readers. -
The state
st
is always evaluated to WHNF and is subject to theNoThunks
check when enabled. - All public functions are exception-safe.
poison :: ( IOLike m, Exception e, HasCallStack ) => RAWLock m st -> ( CallStack -> e) -> m ( Maybe st) Source #
Poison the lock with the given exception. All subsequent access to the lock will result in the given exception being thrown.
Unless the lock has already been poisoned, in which case the original exception with which the lock was poisoned will be thrown.
read :: IOLike m => RAWLock m st -> STM m st Source #
Read the contents of the
RAWLock
in an STM transaction.
Will retry when there is a writer.
In contrast to
withReadAccess
, this transaction will succeed when there
is a writer waiting to write, as there is no IO-operation during which the
lock must be held.
withAppendAccess :: forall m st a. IOLike m => RAWLock m st -> (st -> m (st, a)) -> m a Source #
Access the state stored in the
RAWLock
as an appender.
NOTE: it must be safe to run the given append action concurrently with readers.
Will block when there is another appender, a writer, or when a writer is waiting to take the lock.
withReadAccess :: forall m st a. IOLike m => RAWLock m st -> (st -> m a) -> m a Source #
Access the state stored in the
RAWLock
as a reader.
Will block when there is a writer or when a writer is waiting to take the lock.
withWriteAccess :: forall m st a. IOLike m => RAWLock m st -> (st -> m (st, a)) -> m a Source #
Access the state stored in the
RAWLock
as a writer.
Will block when there is another writer or while there are readers and/or an appender.
Exposed internals: non-bracketed acquire & release
unsafeAcquireAppendAccess :: IOLike m => RAWLock m st -> STM m st Source #
Access the state stored in the
RAWLock
as an appender.
Will block when there is another appender, a writer, or when a writer is waiting to take the lock.
Composable with other
STM
transactions.
NOTE:
must
be followed by a call to
unsafeReleaseAppendAccess
.
unsafeAcquireReadAccess :: IOLike m => RAWLock m st -> STM m st Source #
Acquire the
RAWLock
as a reader.
Will block when there is a writer or when a writer is waiting to take the lock.
Composable with other
STM
transactions.
NOTE:
must
be followed by a call to
unsafeReleaseReadAccess
.
unsafeAcquireWriteAccess :: IOLike m => RAWLock m st -> m st Source #
Access the state stored in the
RAWLock
as a writer.
Will block when there is another writer or while there are readers and/or an appender.
Does
not
compose with other
STM
transactions.
NOTE:
must
be followed by a call to
unsafeReleaseWriteAccess
.
unsafeReleaseAppendAccess Source #
Release the
RAWLock
as an appender.
Doesn't block.
Composable with other
STM
transactions.
NOTE:
must
be preceded by a call to
unsafeAcquireAppendAccess
.
unsafeReleaseReadAccess :: IOLike m => RAWLock m st -> STM m () Source #
Release the
RAWLock
as a reader.
Doesn't block.
Composable with other
STM
transactions.
NOTE:
must
be preceded by a call to
unsafeAcquireReadAccess
.
unsafeReleaseWriteAccess Source #
Release the
RAWLock
as a writer.
Doesn't block.
Does
not
compose with other
STM
transactions.
NOTE:
must
be preceded by a call to
unsafeAcquireWriteAccess
.