choam
choam copied to clipboard
Experiments with composable lock-free concurrency
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
Refs (mutable memory locations with a pure API).- For example, if
xis aRef[Int], thenx.update(_ + 1)is aRxnwhich (when executed) will increment its value.
- For example, if
- Multiple
Rxns can be composed (by using various combinators), and the resultingRxnwill update all affected memory locations atomically.- For example, if
yis also aRef[Int], thenx.update(_ + 1) *> y.update(_ + 1)will increment both of them atomically.
- For example, if
Modules
choam-core:- core types, like
RxnandRef - 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] } - Asynchronous (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[_](note, that theseF[A]operations are – obviously – not lock-free):- queues
- stacks
CountDownLatch
- Integration with asynchronous effect types in
Cats Effect:
choam-stream: integration with FS2Streamschoam-laws: properties fulfilled by the variousRxncombinatorschoam-profiler: JMH profiler "plugin" forRxnstatistics/measurements; enable it with-prof dev.tauri.choam.profiler.RxnProfiler.- 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
Rxnis 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.
Rxnhas a referentially transparent ("pure functional" / "programs as values") API.- The interpreter (that executes
Rxns) is stack-safe. - We also support composing
Rxns which modify the sameRef(thus, anRxnis 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.Emcasimplements 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
Rxnis somewhat similar to a memory transaction, but there are important differences:- A
Rxnis lock-free by construction (but see below); STM transactions are not (necessarily) lock-free (e.g., STM "retry"). - As a consequence of the previous point,
Rxncannot be used to implement "inherently not lock-free" logic (e.g., asynchronously waiting on a condition set by another thread/fiber/similar). However,Rxnis interoperable with async data types which implement Cats Effect typeclasses (see thechoam-asyncmodule). This feature can be used to provide such "waiting" functionality (e.g.,dev.tauri.choam.async.AsyncQueue.unboundedis a queue withenqueueinRxnanddequein, e.g.,IOor another asyncF[_]). - The implementation (the
Rxninterpreter) 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);Rxnhas no such feature (but of course return values can encode errors withOption,Either, or similar). - Some STM systems allow access to transactional memory from
non-transactional code;
Rxndoesn't support this, the contents of anr: Ref[A]can only be accessed from inside aRxn(although there is a read-only almost "escape hatch":r.unsafeDirectRead).
- A
- Similarities between
Rxns and STM transactions include the following:- atomicity
- consistency
- isolation
Rxnalso 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 runningRxns (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
Rxns 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
Compatibility and assumptions
"Early" SemVer 2.0.0 binary backwards compatibility, with the following exceptions:
- The versions of
choam-modules must match exactly (e.g., don't use"choam-data" % "0.4.1"with"choam-core" % "0.4.0").- In sbt ⩾ 1.10.0 this can be ensured like this:
csrSameVersions += Set(sbt.librarymanagement.InclExclRule("dev.tauri", "choam-*"))
- In sbt ⩾ 1.10.0 this can be ensured like this:
- There is no backwards compatibility for APIs which are
- inside
*.internal.*packages (e.g.,dev.tauri.choam.internal.mcas); - called
unsafe*(e.g.,Rxn.unsafe.retryorRef#unsafeCas).
- inside
- There is no backwards compatibility for these modules:
choam-lawschoam-streamchoam-profiler- (and all unpublished modules)
- There is no backwards compatibility for "hash" versions (e.g.,
0.4-39d987a; these are not even SemVer compatible).
Supported platforms:
- Platforms:
- JVM:
- versions ⩾ 11
- tested on OpenJDK, Graal, and OpenJ9 (but should work on others)
Rxn.secureRandomandUUIDGenboth need either theWindows-PRNGor (/dev/randomand/dev/urandom) to be available
- Scala.js:
- works, but not really useful (we assume no multithreading)
- provided to ease cross-compiling
- JVM:
- Scala versions: cross-compiled for 2.13 and 3.3
Lock-freedom
Rxns are lock-free by construction, if the following assumptions hold:
- No "inifinite loops" are created (e.g., by recursive
flatMaps) - No
unsafeoperations are used (e.g.,Rxn.unsafe.retryis obviously not lock-free) - We assume instances of
FunctionNto be pure and total - We assume that certain JVM operations are lock-free:
VarHandleoperations (e.g.,compareAndSet)- in practice, this is true only on 64-bit platforms
- GC and classloading
- in practice, this is not true
ThreadLocalRandom,ThreadLocal
- Certain
Rxnoperations require extra assumptions:Rxn.secureRandomandUUIDGenuse the OS RNG, which may block (although we really try to use the non-blocking ones)- operations in
F[_]might not be lock-free (an obvious example isPromise#get) - in
choam-asyncwe assume that calling a CEAsynccallback is lock-free (incats.effect.IO, as of version 3.5.4, this is not technically true)
- Executing a
Rxnwith aRxn.Strategyother thanRxn.Strategy.Spinis not lock-free - Only
Mcas.DefaultMcasis lock-free, otherMcasimplementations may not be