CHOAM
Experiments with composable lock-free concurrency
Overview
The type Rxn[-A, +B]
is similar to an effectful function from A
to B
(that is, A => F[B]
), but:
- The only effect it can perform is lock-free updates to
Ref
s (mutable memory locations with a pure API).- For example, if
x
is aRef[Int]
, thenx.update(_ + 1)
is aRxn
which (when executed) will increment its value.
- For example, if
- Multiple
Rxn
s can be composed (by using various combinators), and the resultingRxn
will update all affected memory locations atomically.- For example, if
y
is also aRef[Int]
, thenx.update(_ + 1) *> y.update(_ + 1)
will increment both of them atomically.
- For example, if
Modules
choam-core
:- core types, like
Rxn
andRef
- integration with synchronous effect types in Cats Effect
- core types, like
choam-data
: concurrent data structures:- queues
- stacks
- hash- and ordered maps and sets
- counter
choam-async
:- integration with asynchronous effect types in
Cats Effect:
- the main integration point is a
Promise
, which can be completed as aRxn
, and can be waited on as an asyncF[_]
:trait Promise[F[_], A] { def complete: Rxn[A, Boolean] def get: F[A] }
- async (dual) data structures can be built on this primitive
- the main integration point is a
- async data structures; some of their operations are
semantically blocking (i.e., fiber blocking
),
and so are in an async
F[_]
):- queues
- stacks
- integration with asynchronous effect types in
Cats Effect:
choam-stream
: integration with FS2Stream
schoam-laws
: properties fulfilled by the variousRxn
combinators- Internal modules (don't use them directly):
choam-mcas
: low-level multi-word compare-and-swap (MCAS/k-CAS) implementationschoam-skiplist
: a concurrent skip list map for internal use
JARs are on Maven Central. Browsable Scaladoc is available here.
Related work
- Our
Rxn
is an extended version of reagents, described in Reagents: Expressing and Composing Fine-grained Concurrency . (Other implementations or reagents: Scala, OCaml, Racket.) The main diferences from the paper are:- Only lock-free features (and a few low-level ones) are implemented.
Rxn
has a referentially transparent ("pure functional" / "programs as values") API.- The interpreter (that executes
Rxn
s) is stack-safe. - We also support composing
Rxn
s which modify the sameRef
(thus, anRxn
is closer to an STM transaction than a reagent; see below). - Reads are always guaranteed to be consistent (this is called opacity, see below).
- Multi-word compare-and-swap (MCAS/k-CAS) implementations:
- A Practical Multi-Word Compare-and-Swap Operation (an earlier version used this algorithm)
- Efficient Multi-word Compare and Swap
(
Mcas.Emcas
implements a variant of this algorithm; this is the default algorithm we use on the JVM) - A simple, non-lock-free algorithm from the Reagents paper (see above) is implemented as
Mcas.SpinLockMcas
(we use it for testing)
- Software transactional memory (STM)
- A
Rxn
is somewhat similar to a memory transaction, but there are important differences:- A
Rxn
is lock-free by construction (unless it's infinitely recursive, or anunsafe
method was used to create it); STM transactions are not necessarily lock-free (e.g., STM "retry"). - As a consequence of the previous point,
Rxn
cannot be used to implement "inherently not lock-free" logic (e.g., asynchronously waiting on a condition set by another thread/fiber/similar). However,Rxn
is interoperable with async data types which implement Cats Effect typeclasses (see thechoam-async
module). This feature can be used to provide such "waiting" functionality (e.g.,dev.tauri.choam.async.AsyncQueue.ringBuffer
is a queue withenqueue
inRxn
anddeque
in, e.g.,IO
or another asyncF[_]
). - The implementation (the
Rxn
interpreter) is also lock-free; STM implementations are usually not (although there are exceptions). - STM transactions usually have a way of raising/handling errors
(e.g.,
MonadError
);Rxn
has no such feature (of course return values can encode errors withOption
,Either
, or similar). - Some STM systems allow access to transactional memory from
non-transactional code;
Rxn
doesn't support this, the contents of anr: Ref[A]
can only be accessed from inside aRxn
(although there is a read-only escape hatch:r.unsafeDirectRead
).
- A
- Similarities between
Rxn
s and STM transactions include the following:- atomicity
- consistency
- isolation
Rxn
also provides a correctness property called opacity; a lot of STM implementations also guarantee this property (e.g.,scala-stm
), but not all of them. Opacity basically guarantees that all observed values are consistent with each other, even in runningRxn
s (some STM systems only guarantee such consistency for transactions which actually commit).
- Some STM implementations:
- Haskell:
Control.Concurrent.STM
. - Scala:
scala-stm
,cats-stm
,ZSTM
. - TL2
and SwissTM:
the system which guarantees opacity (see above) for
Rxn
s is based on the one in SwissTM (which is itself based on the one in TL2). However, TL2 and SwissTM are lock-based STM implementations; our implementation is lock-free.
- Haskell:
- A