We prefer to work with immutable types in functional-programming. However, it's not always possible, and sometimes we need some shared mutable state. With the immutable types in this library you'd need to protect the updates with locks:
// Some global
static HashSet<int> set = HashSet(1, 2, 3);
static object sync = new();
lock(sync)
{
set = set.Add(4);
}
This in unsatisfactory, and so this module is all about lock-free atomic operations. Atom
allows you to protect any
value. AtomHashMap
and AtomSeq
are HashMap
and Seq
wrapped up into a lock-free mutable structure. Snapshots
of those are free! The above code can be written:
static AtomHashSet<int> set = AtomHashSet(1, 2, 3);
set.Add(4);
Finally, there's the Software Transactional Memory (STM) system. Which allows for transactional changes to multiple
Ref
values. Ref
just wrap up access to a value, and allows the state changes to be tracked by the STM
.
See the concurrency section of the wiki for more info.
- LastWriteWins <V>
- FirstWriteWins <V>
- Conflict <V>
- Prelude
- Ref <A> (A value, Func<A, bool>? validator = null)
- atomic <R> (Func<R> op, Isolation isolation = Isolation.Snapshot)
- atomic (Action op, Isolation isolation = Isolation.Snapshot)
- snapshot <R> (Func<R> op)
- snapshot (Action op)
- serial <R> (Func<R> op)
- serial (Action op)
- swap <A> (Ref<A> r, Func<A, A> f)
- commute <A> (Ref<A> r, Func<A, A> f)
- Atom <A> (A value)
- Atom <A> (A value, Func<A, bool> validator)
- Atom <M, A> (M metadata, A value)
- Atom <M, A> (M metadata, A value, Func<A, bool> validator)
- swap <A> (Atom<A> ma, Func<A, A> f)
- swap <A> (Atom<A> ma, Func<A, Option<A>> f)
- swap <M, A> (Atom<M, A> ma, Func<M, A, A> f)
- swap <M, A> (Atom<M, A> ma, Func<M, A, Option<A>> f)
Sub modules
Atom |
AtomHashMap |
AtomQue |
AtomSeq |
STM |
Task |
ValueTask |
VectorClock |
VersionHashMap |
VersionVector |
struct LastWriteWins <V> Source #
Last-write-wins conflict resolver
struct FirstWriteWins <V> Source #
First-write-wins conflict resolver
interface Conflict <V> Source #
Trait that defines how to deal with a conflict between two values
type | V | Value type |
method Ref<A> Ref <A> (A value, Func<A, bool>? validator = null) Source #
Generates a new reference that can be used within a sync
transaction
Refs
ensure safe shared use of mutable storage locations via a software transactional
memory (STM) system. Refs
are bound to a single storage location for their lifetime,
and only allow mutation of that location to occur within a transaction.
Transactions (within a sync(() => ...)
) should be easy to understand if you’ve ever used database
transactions - they ensure that all actions on Refs are atomic, consistent, and isolated.
- Atomic - means that every change to Refs made within a transaction occurs or none do.
- Consistent - means that each new value can be checked with a validator function before allowing the transaction to commit.
- Isolated - means that no transaction sees the effects of any other transaction while it is running.
Another feature common to STMs is that, should a transaction have a conflict while running, it is automatically retried. The language-ext STM uses multi-version concurrency control for snapshot and serialisable isolation.
In practice, this means:
All reads of Refs will see a consistent snapshot of the Ref world as of the starting point of the transaction (its 'read point'). The transaction will see any changes it has made. This is called the in-transaction-value.
All changes made to Refs during a transaction will appear to occur at a single point in the Ref world timeline (its 'write point').
No changes will have been made by any other transactions to any Refs that have been modified by this transaction.
Readers will never block writers, or other readers.
Writers will never block readers.
I/O and other activities with side-effects should be avoided in transactions, since transactions will be retried.
If a constraint on the validity of a value of a Ref that is being changed depends upon the
simultaneous value of a Ref that is not being changed, that second Ref can be protected from
modification by running the sync
transaction with Isolation.Serialisable
.
The language-ext STM is designed to work with the persistent collections (Map
, HashMap
,
Seq
, Lst
, Set,
HashSet` etc.), and it is strongly recommended that you use the language-ext
collections as the values of your Refs. Since all work done in an STM transaction is speculative,
it is imperative that there be a low cost to making copies and modifications. Persistent collections
have free copies (just use the original, it can’t be changed), and 'modifications' share structure
efficiently. In any case:
The values placed in Refs must be, or be considered, immutable. Otherwise, this library can’t help you.
See the concurrency section of the wiki for more info.
param | value | Initial value of the ref |
param | validator | Validator that is called on the ref value just
before any transaction is committed (within a |
method R atomic <R> (Func<R> op, Isolation isolation = Isolation.Snapshot) Source #
Snapshot isolation requires that nothing outside the transaction has written to any of the values that are written-to within the transaction. If anything does write to the values used within the transaction, then the transaction is rolled back and retried (using the latest 'world' state).
Serialisable isolation requires that nothing outside the transaction has written to any of the values that are read-from or written-to within the transaction. If anything does read from or written to the values used within the transaction, then it is rolled back and retried (using the latest 'world' state).
It is the strictest form of isolation, and the most likely to conflict; but protects against cross read/write inconsistencies. For example, if you have:
var x = Ref(1);
var y = Ref(2);
snapshot(() => x.Value = y.Value + 1);
Then something writing to y
mid-way through the transaction would not cause the transaction to fail.
Because y
was only read-from, not written to. However, this:
var x = Ref(1);
var y = Ref(2);
serial(() => x.Value = y.Value + 1);
... would fail if something wrote to y
.
method Unit atomic (Action op, Isolation isolation = Isolation.Snapshot) Source #
Run the op within a new transaction If a transaction is already running, then this becomes part of the parent transaction
Snapshot isolation requires that nothing outside the transaction has written to any of the values that are written-to within the transaction. If anything does write to the values used within the transaction, then the transaction is rolled back and retried (using the latest 'world' state).
Serialisable isolation requires that nothing outside the transaction has written to any of the values that are read-from or written-to within the transaction. If anything does read from or written to the values used within the transaction, then it is rolled back and retried (using the latest 'world' state).
It is the strictest form of isolation, and the most likely to conflict; but protects against cross read/write inconsistencies. For example, if you have:
var x = Ref(1);
var y = Ref(2);
snapshot(() => x.Value = y.Value + 1);
Then something writing to y
mid-way through the transaction would not cause the transaction to fail.
Because y
was only read-from, not written to. However, this:
var x = Ref(1);
var y = Ref(2);
serial(() => x.Value = y.Value + 1);
... would fail if something wrote to y
.
method R snapshot <R> (Func<R> op) Source #
Run the op within a new transaction If a transaction is already running, then this becomes part of the parent transaction
Snapshot isolation requires that nothing outside the transaction has written to any of the values that are written-to within the transaction. If anything does write to the values used within the transaction, then the transaction is rolled back and retried (using the latest 'world' state).
method Unit snapshot (Action op) Source #
Run the op within a new transaction If a transaction is already running, then this becomes part of the parent transaction
Snapshot isolation requires that nothing outside the transaction has written to any of the values that are written-to within the transaction. If anything does write to the values used within the transaction, then the transaction is rolled back and retried (using the latest 'world' state).
method R serial <R> (Func<R> op) Source #
Run the op within a new transaction If a transaction is already running, then this becomes part of the parent transaction
Serialisable isolation requires that nothing outside the transaction has written to any of the values that are read-from or written-to within the transaction. If anything does read from or written to the values used within the transaction, then it is rolled back and retried (using the latest 'world' state).
It is the strictest form of isolation, and the most likely to conflict; but protects against cross read/write inconsistencies. For example, if you have:
var x = Ref(1);
var y = Ref(2);
snapshot(() => x.Value = y.Value + 1);
Then something writing to y
mid-way through the transaction would not cause the transaction to fail.
Because y
was only read-from, not written to. However, this:
var x = Ref(1);
var y = Ref(2);
serial(() => x.Value = y.Value + 1);
... would fail if something wrote to y
.
method Unit serial (Action op) Source #
Run the op within a new transaction If a transaction is already running, then this becomes part of the parent transaction
Serialisable isolation requires that nothing outside the transaction has written to any of the values that are read-from or written-to within the transaction. If anything does read from or written to the values used within the transaction, then it is rolled back and retried (using the latest 'world' state).
It is the strictest form of isolation, and the most likely to conflict; but protects against cross read/write inconsistencies. For example, if you have:
var x = Ref(1);
var y = Ref(2);
snapshot(() => x.Value = y.Value + 1);
Then something writing to y
mid-way through the transaction would not cause the transaction to fail.
Because y
was only read-from, not written to. However, this:
var x = Ref(1);
var y = Ref(2);
serial(() => x.Value = y.Value + 1);
... would fail if something wrote to y
.
method A swap <A> (Ref<A> r, Func<A, A> f) Source #
Swap the old value for the new returned by f
Must be run within a sync
transaction
param | r |
|
param | f | Function to update the |
returns | The value returned from |
method A commute <A> (Ref<A> r, Func<A, A> f) Source #
Must be called in a transaction. Sets the in-transaction-value of ref to:
`f(in-transaction-value-of-ref)`
and returns the in-transaction-value when complete.
At the commit point of the transaction, f
is run AGAIN with the
most recently committed value:
`f(most-recently-committed-value-of-ref)`
Thus f
should be commutative, or, failing that, you must accept
last-one-in-wins behavior.
Commute allows for more concurrency than just setting the Ref's value
method Atom<A> Atom <A> (A value) Source #
Atoms provide a way to manage shared, synchronous, independent state without locks.
The intended use of atom is to hold one an immutable data structure. You change
the value by applying a function to the old value. This is done in an atomic
manner by Swap
.
Internally, Swap
reads the current value, applies the function to it, and
attempts to CompareExchange
it in. Since another thread may have changed the
value in the intervening time, it may have to retry, and does so in a spin loop.
The net effect is that the value will always be the result of the application of the supplied function to a current value, atomically. However, because the function might be called multiple times, it must be free of side effects.
Atoms are an efficient way to represent some state that will never need to be coordinated with any other, and for which you wish to make synchronous changes.
param | value | Initial value of the atom |
returns | The constructed Atom |
method Option<Atom<A>> Atom <A> (A value, Func<A, bool> validator) Source #
Atoms provide a way to manage shared, synchronous, independent state without locks.
The intended use of atom is to hold one an immutable data structure. You change
the value by applying a function to the old value. This is done in an atomic
manner by Swap
.
Internally, Swap
reads the current value, applies the function to it, and
attempts to CompareExchange
it in. Since another thread may have changed the
value in the intervening time, it may have to retry, and does so in a spin loop.
The net effect is that the value will always be the result of the application of the supplied function to a current value, atomically. However, because the function might be called multiple times, it must be free of side effects.
Atoms are an efficient way to represent some state that will never need to be coordinated with any other, and for which you wish to make synchronous changes.
param | value | Initial value of the atom |
param | validator | Function to run on the value after each state change. If the function returns false for any proposed new state, then the |
returns | The constructed Atom or None if the validation faled for the initial
|
method Atom<M, A> Atom <M, A> (M metadata, A value) Source #
Atoms provide a way to manage shared, synchronous, independent state without locks.
The intended use of atom is to hold one an immutable data structure. You change
the value by applying a function to the old value. This is done in an atomic
manner by Swap
.
Internally, Swap
reads the current value, applies the function to it, and
attempts to CompareExchange
it in. Since another thread may have changed the
value in the intervening time, it may have to retry, and does so in a spin loop.
The net effect is that the value will always be the result of the application of the supplied function to a current value, atomically. However, because the function might be called multiple times, it must be free of side effects.
Atoms are an efficient way to represent some state that will never need to be coordinated with any other, and for which you wish to make synchronous changes.
param | metadata | Metadata to be passed to the validation function |
param | value | Initial value of the atom |
returns | The constructed Atom |
method Option<Atom<M, A>> Atom <M, A> (M metadata, A value, Func<A, bool> validator) Source #
Atoms provide a way to manage shared, synchronous, independent state without locks.
The intended use of atom is to hold one an immutable data structure. You change
the value by applying a function to the old value. This is done in an atomic
manner by Swap
.
Internally, Swap
reads the current value, applies the function to it, and
attempts to CompareExchange
it in. Since another thread may have changed the
value in the intervening time, it may have to retry, and does so in a spin loop.
The net effect is that the value will always be the result of the application of the supplied function to a current value, atomically. However, because the function might be called multiple times, it must be free of side effects.
Atoms are an efficient way to represent some state that will never need to be coordinated with any other, and for which you wish to make synchronous changes.
param | metadata | Metadata to be passed to the validation function |
param | value | Initial value of the atom |
param | validator | Function to run on the value after each state change. If the function returns false for any proposed new state, then the |
returns | The constructed Atom or None if the validation faled for the initial
|
method A swap <A> (Atom<A> ma, Func<A, A> f) Source #
Atomically updates the value by passing the old value to f
and updating
the atom with the result. Note: f
may be called multiple times, so it
should be free of side effects.
param | f | Function to update the atom |
returns | If the swap operation succeeded then a snapshot of the value that was set is returned.
If the swap operation fails (which can only happen due to its validator returning false),
then a snapshot of the current value within the Atom is returned.
If there is no validator for the Atom then the return value is always the snapshot of
the successful |
method A swap <A> (Atom<A> ma, Func<A, Option<A>> f) Source #
Atomically updates the value by passing the old value to f
and updating
the atom with the result. Note: f
may be called multiple times, so it
should be free of side effects.
param | f | Function to update the atom |
returns |
|
method A swap <M, A> (Atom<M, A> ma, Func<M, A, A> f) Source #
Atomically updates the value by passing the old value to f
and updating
the atom with the result. Note: f
may be called multiple times, so it
should be free of side effects.
param | f | Function to update the atom |
returns | If the swap operation succeeded then a snapshot of the value that was set is returned.
If the swap operation fails (which can only happen due to its validator returning false),
then a snapshot of the current value within the Atom is returned.
If there is no validator for the Atom then the return value is always the snapshot of
the successful |
method A swap <M, A> (Atom<M, A> ma, Func<M, A, Option<A>> f) Source #
Atomically updates the value by passing the old value to f
and updating
the atom with the result. Note: f
may be called multiple times, so it
should be free of side effects.
param | f | Function to update the atom |
returns |
|