Experiments with composable lock-free concurrency
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
libraryDependencies += "dev.tauri" %%% "choam-core" % choamVersion // see above for latest version
The choam-core
module contains the fundamental types for working with Rxn
s.
For more modules, see below.
The complete version of the example above, which increments the value of
two Ref
s is as follows:
import dev.tauri.choam.{ Ref, Rxn }
def incrBoth(x: Ref[Int], y: Ref[Int]): Rxn[Any, Unit] = {
x.update(_ + 1) *> y.update(_ + 1)
}
It can be executed with (for example) Cats Effect like this:
import cats.effect.IO
import dev.tauri.choam.Reactive
implicit val reactiveForIo: Reactive[IO] =
Reactive.forSync[IO]
val myTask: IO[Unit] = for {
// create two refs:
x <- Ref.unpadded(0).run[IO]
y <- Ref.unpadded(42).run[IO]
// increment their values atomically:
_ <- incrBoth(x, y).run[IO]
} yield ()
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] }
- 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 FS2Stream
schoam-laws
: properties fulfilled by the variousRxn
combinatorschoam-profiler
: JMH profiler "plugin" forRxn
statistics/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.
- 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 (but see below); 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.unbounded
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 (but 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 almost "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
"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 when:
- using APIs which are non-public in Scala (even though some of these are public in the bytecode);
- inheriting
sealed
classes/traits (even though this may not be enforced by the bytecode); - using
*.internal.*
packages (e.g.,dev.tauri.choam.internal.mcas
); - using
unsafe*
APIs (e.g.,Rxn.unsafe.retry
orRef#unsafeCas
).
- There is no backwards compatibility for these modules:
choam-stm
choam-mcas
choam-skiplist
choam-laws
choam-stream
choam-profiler
choam-docs
- (and all unpublished modules)
- There is no backwards compatibility for "hash" versions (e.g.,
0.4-39d987a
; these are not even SemVer compatible).
- Platforms:
- JVM:
- versions ⩾ 11
- tested on OpenJDK, Graal, and OpenJ9 (but should work on others)
Rxn.secureRandom
andUUIDGen
both need either theWindows-PRNG
or (/dev/random
and/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
Rxn
s are lock-free by construction, if the following assumptions hold:
- No "inifinite loops" are created (e.g., by recursive
flatMap
s) - No
unsafe
operations are used (e.g.,Rxn.unsafe.retry
is obviously not lock-free) - We assume instances of
FunctionN
to be pure and total - We assume that certain JVM operations are lock-free:
VarHandle
operations (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
Rxn
operations require extra assumptions:Rxn.secureRandom
andUUIDGen
use 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-async
we assume that calling a CEAsync
callback is lock-free (incats.effect.IO
, as of version 3.5.4, this is not technically true)
- Executing a
Rxn
with aRxn.Strategy
other thanRxn.Strategy.Spin
is not lock-free - Only
Mcas.DefaultMcas
is lock-free, otherMcas
implementations may not be