• Stars
    star
    126
  • Rank 284,543 (Top 6 %)
  • Language
    OCaml
  • License
    ISC License
  • Created over 9 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Reagents for multicore OCaml

= πŸ“’ Note πŸ“’ =

kcas is the recommended framework for composable concurrency. It provides a favourable performance and safer interface than Reagents.

Reagents are a research project and not ready for production use. This repository is no longer actively developed or maintained.

See comparison for details.

Reagents β€” Composable lock-free data and synchronization structures

API reference

Reagents are an experimental library for writing multicore programs. Reagents provide an expressive framework for composable multithreading. They support both fine- and coarse-grained multithreading and incorporate mechanisms for efficient retrying.

Contents

Motivation

Reagents strive to be a comprehensive framework for all things concurrent and parallel. In particular, they have the following advantages over traditional approaches:

  • Composability. Operations are trivial to compose with the set of provided composition operators. An item can be moved from one lock-free stack to another in a single atomic lock-free step. Further, a release of one lock can be melded with the acquisition of another one. This has far-reaching consequences. For example, a simple way to implement an LRU cache is using a queue and a map, but traditional implementations of thread-safe queue and map do not help as both have to be updated at once. Reagents make that trivial (in contrast with implementing thread-safe LRU cache from scratch) and let us maintain useful properties (in contrast with surrounding both updates with a lock).

  • Expressiveness. Reagents provide building blocks for various multithreading patterns: communicating by sharing memory and message passing, active and passive invocation of operations, conjunction (pair) and disjunction (choice) of operations.

  • Fine-grained multithreading. Low-level synchronisation primitives tend to perform and scale better than high-level ones. Reagents use fine-grained multithreading internally and expose it for expert users.

  • Efficient retrying. Reagents parametrise over the scheduler to suspend and resume fibers as the conditions for their progress are met or not. This makes common anti-patterns such as busy-waiting easy to avoid.

Limitations

Reagents are weaker than transactional memory. A reagent must be decomposable into a list of compare-and-set operations. This eliminates the need for any extra accounting for performed CASes.

Getting Reagents

Reagents require OCaml 5 (opam switch create 5.0.0).

Install Reagents from this repository.

opam pin -y https://github.com/ocaml-multicore/reagents.git

Soon Reagents will be available in the opam repository.

Test the setup in utop with the following snippet.

# #require "reagents";;

# module Scheduler = (val Reagents.Toy_scheduler.make 1 ())
  open Reagents.Make (Scheduler);;

# let s = Ref.mk_ref "hello world\n" in
  Scheduler.run (run (Ref.read s >>> lift print_string));;

hello world
- : unit = ()

Key concepts

This section briefly explains all the key concepts required for using Reagents.

Scheduler

Reagents are parametrised over a minimal scheduler interface. If an active reagent cannot make progress, Reagents automatically suspend the fiber. Once someone else updates the state, Reagents trigger required resumptions. This behavior comes for free.

  type 'a cont

  val suspend : ('a cont -> 'a option) -> 'a
  val resume : 'a cont -> 'a -> unit
  val get_tid : unit -> int

A toy scheduler for experimenting and running tests is available in Reagents.Toy_scheduler.

Reagent Type

A computation within Reagents framework has the following type ('a, 'b) Reagents.t. Here, the computation takes a parameter of type 'a and returns a value of type 'b. Internally, it may consist of any number of operations and any number of side effects. Crucially, regardless of the construction of a reagent, all of its operations execute atomically and entirely or none at all.

Combinators

Reagents can be composed in arbitrary ways. The following three main combinators are exposed by the library. These can be composed into more complex combinators.

  • val (>>>) : ('a,'b) t -> ('b,'c) t -> ('a,'c) t

Sequential composition runs one reagent after another. It passes the result of the first one as a parameter to the second one.

  • val (<*>) : ('a,'b) t -> ('a,'c) t -> ('a,'b * 'c) t

Conjunction executes both reagents at once and returns both results. Note, this combinator still attempts to execute its components sequentially. It differs with [>>>] in information flow only.

  • val (<+>) : ('a,'b) t -> ('a,'b) t -> ('a,'b) t

Disjunction tries to execute the first reagent and if it blocks, it attempts the second one. If both block, the first one to unblock is executed. Also referred to as a left-biased choice.

Running a Reagent

Once the desired reagent has been composed, it can be run.

val run : ('a, 'b) t -> 'a -> 'b

run r v executes the reagent r with value v.

Note, this function has to be executed from within a scheduler for the suspension and resumption effects to be handled correctly.

Others

There are several other values defined in the public interface that serve as units, helpers, or transformations for existing reagents. Perhaps the most notable one is attempt, which converts a blocking reagent into a non-blocking one.

val attempt : ('a, 'b) t -> ('a, 'b option) t

attempt r converts a blocking reagent into a non-blocking one. If the reagent blocks, then attempt returns None. Otherwise, the reagent is committed immediately and the returned option value is non-empty.

Data structures

Reagents expose two core data structures. Complex data structures should utilise these as building blocks, if possible.

  • Reference β€” a low-level object akin to an 'a Atomic.t, which can be modified using compare-and-set operation. In contrast with the standard library's atomic, if the expected value does not match it is going to suspend until the operation can succeed (in the default case).

  • Channel β€” a two-way channel for sharing memory by communicating.

The library also provides many higher-level data structures (e.g. counter, stack, queue) and synchronisation primitives (e.g. lock, conditional variable). See interface.

Sample programs

This section showcases a few applications of Reagents.

Counter

See a simple example of creating a synchronized counter below.

# let counter = Counter.create 0 in
  let a = run (Counter.inc counter) () in
  let b = run (Counter.inc counter) () in
  let c = run (Counter.dec counter) () in
  (a, b, c);;
- : int * int * int = (0, 1, 2)

Both inc and dec operations are of type (unit, int) Reagents.t since they take a unit as input and return the previous value. Now, imagine there are several counters representing different statistics about the system, balances of bank accounts, etc.

Reagents let us take a consistent snapshot of the system without locks.

# let c1 = Counter.create 0 in
  let c2 = Counter.create 0 in

  run (Counter.get c1 <*> Counter.get c2) ();;
- : int * int = (0, 0)

Reference

Continuing from counter, we can update any number of locations at once as well. This example uses references, which are similar to an atomic variable.

# let account_1 = Ref.mk_ref 100 in
  let account_2 = Ref.mk_ref 0 in

  let transfer a b amount =
    Ref.upd a (fun acc () -> Some (acc - amount, ()))
    >>> Ref.upd b (fun acc () -> Some (acc + amount, ()))
  in

  run (transfer account_1 account_2 100) ();

  ((Ref.read_imm account_1), (Ref.read_imm account_2));;
- : int * int = (0, 100)

Note, in the example above the function passed to Ref.upd returns an option type. If the observed value of account is not appropriate for the requested operation (e.g. the transfer would make account 1 negative), it may choose to return None. In such a case, reagent will block until the value of the reference is updated by another actor. Alternatively, it can be attempted, to simply return with failure if the reagent cannot proceed. See counter_test.ml for example.

Channel

Channel is the building block for sharing memory by communication. Reagents offer a two-way channel (but we can pass units in one direction).

# Scheduler.run (fun () ->
    let endpoint_a, endpoint_b = Channel.mk_chan () in
    Scheduler.fork (fun () -> run (Channel.swap endpoint_a) 12345);
    print_int (run (Channel.swap endpoint_b) ()));;
12345- : unit = ()

There are a couple of nuances worth keeping in mind:

  • Channels have a blocking nature; the reaction can occur only if there are two matching reagents ready to interact. Thus, the one to arrive first is going to block until its match is ready.
  • Since <*> is not truly parallel, there are some limitations to the type of channel reactions Reagents are able to commit. See pair_not_parallel.ml for more details.

Catalyst

Catalyst is a passively invoked reagent. It does not react on its own, instead, it remains ready to react with others as many times as needed until canceled. Catalysts let us link multiple data structures to form a graph of computations. See catalyst_test.ml for examples of linking channels.

More

More sample programs and tests are located in the tests directory of the distribution. They can be built and run with:

dune build @runtest

Individual tests are built as executables (available in Dune's _build directory).

Development

Formatting

This project uses ocamlformat (for OCaml) and prettier (for Markdown).

Internals quick start

Reagents are largely driven by kcas. kcas is a software solution for executing multiple atomic operations as a single transaction on architectures providing a single-word CAS only. The current implementation of kcas requires k+1 atomic operations for k-location update.

In the non-blocking case, Reagents constitute a convenient abstraction over the specification and aggregation of individual atomic operations. If the list of required atomic operations can be constructed and committed immediately, a reagent succeeds using the fast-path.

However, an operation may be unable to proceed. If fast-path found that the operation cannot finish (e.g. pop on an empty stack), Reagents core generates an offer. The offer is then published in a relevant queue with extra information and fiber suspends on it. Once another thread comes, it sees the offer and resumes fibers that are now unblocked. This logic is reagent-specific. In the case of reference, it's going to wake up all waiters. In the case of channel, it will take suspended thread's transaction, merge it with its own, and try to commit everything at once. If the commit succeeds, it provides the suspended thread with the result and resumes it. Both actions cancel the offer.

These two mechanisms are the key design choices behind Reagents.

License

Reagents are distributed under ISC license.

Further Reading

Talks:

Papers in the order of increasing detail:

More Repositories

1

ocaml-multicore

Multicore OCaml
OCaml
763
star
2

ocaml-effects-tutorial

Concurrent Programming with Effect Handlers
OCaml
660
star
3

eio

Effects-based direct-style IO for multicore OCaml
OCaml
555
star
4

effects-examples

Examples to illustrate the use of algebraic effects in Multicore OCaml
OCaml
423
star
5

parallel-programming-in-multicore-ocaml

Tutorial on Multicore OCaml parallel programming with domainslib
OCaml
283
star
6

ocaml5-tutorial

A hands-on tutorial on the new parallelism features in OCaml 5
OCaml
200
star
7

saturn

Lock-free data structures for multicore OCaml
OCaml
197
star
8

domainslib

Parallel Programming over Domains
OCaml
171
star
9

awesome-multicore-ocaml

A collection of libraries, experiments and ideas relating to OCaml 5 (multicore + effects)
147
star
10

kcas

Software Transactional Memory for OCaml
OCaml
107
star
11

picos

Interoperable effects based concurrency
OCaml
86
star
12

meio

Monitor Eio programs
OCaml
78
star
13

ocaml-uring

Bindings to io_uring for OCaml
OCaml
62
star
14

multicore-opam

OPAM repo for OCaml multicore development
53
star
15

multicoretests

PBT testsuite and libraries for testing multicore OCaml
OCaml
37
star
16

lwt_eio

Use Lwt libraries from within Eio
OCaml
34
star
17

dscheck

Experimental model checker for testing concurrent algorithms
OCaml
33
star
18

eventlog-tools

Tools for the runtime tracing in OCaml 4.11.0 and higher
OCaml
31
star
19

multicore-talks

Repository containing slides and examples from the 2020 OCaml Workshop talk on "Parallelising your OCaml code with Multicore OCaml"
TeX
30
star
20

ocaml-iomux

Io multiplexers bindings for ocaml (poll/kqueue/epoll and so on)
OCaml
28
star
21

retro-httpaf-bench

Benchmarking environment for http servers
Jupyter Notebook
21
star
22

icfp-2023-eio-tutorial

Lwt to Eio tutorial
OCaml
20
star
23

ocaml-tsan

Race detection in OCaml using the ThreadSanitizer runtime analysis.
OCaml
20
star
24

par_incr

Parallel version of incremental library
OCaml
19
star
25

ocaml-iocp

OCaml bindings to Windows' IOCP API
OCaml
17
star
26

uring-trace

Visualization tool for your IO-uring workload
C
14
star
27

hdr_histogram_ocaml

C
12
star
28

tezos

Tezos running on Multicore OCaml
OCaml
12
star
29

multicore-magic

Low-level multicore utilities for OCaml
OCaml
12
star
30

domain-local-await

A scheduler independent blocking mechanism
OCaml
12
star
31

multicore-ocaml-verify

Verifying bits of Multicore OCaml implementation
Makefile
11
star
32

eio-trace

Trace visualisation tool for Eio programs
OCaml
11
star
33

parafuzz

Property-based grey-box fuzzing for Multicore OCaml
OCaml
9
star
34

backoff

Exponential backoff mechanism
OCaml
9
star
35

eio_js

Eio for JavaScript environments
OCaml
8
star
36

multicore-bench

Framework for benchmarking on multiple cores on current-bench
OCaml
8
star
37

docs

Docs
5
star
38

thread-table

A lock-free thread-safe integer keyed hash table
OCaml
5
star
39

domain-local-timeout

A scheduler independent timeout mechanism
OCaml
4
star
40

fun-ocaml-workshop

Fun OCaml workshop
OCaml
4
star
41

dwarf_validator

Tool for validating wellformedness of DWARF unwind information
Python
3
star
42

eio_browser

Eio backend for the browser
OCaml
3
star
43

single-use-event

A scheduler agnostic blocking mechanism
OCaml
1
star