From c519fa45b81832eba0aad4acec097e0fc4058423 Mon Sep 17 00:00:00 2001 From: c-cube Date: Wed, 4 Sep 2024 16:10:21 +0000 Subject: [PATCH] deploy: 9b3c75124e5f66e14d63a1d930be8953df03ae98 --- backoff/Backoff/index.html | 2 + backoff/_doc-dir/CHANGES.md | 7 + backoff/_doc-dir/LICENSE.md | 16 + backoff/_doc-dir/README.md | 94 +++ backoff/index.html | 2 + index.html | 2 +- .../For_runner_implementors/index.html | 4 +- .../Moonpool/Background_thread/index.html | 2 +- moonpool/Moonpool/Exn_bt/index.html | 2 +- .../For_runner_implementors/index.html | 4 +- moonpool/Moonpool/Fifo_pool/index.html | 2 +- moonpool/Moonpool/Fut/index.html | 2 +- .../Runner/For_runner_implementors/index.html | 4 +- moonpool/Moonpool/Runner/index.html | 2 +- .../Task_local_storage/Direct/index.html | 2 - .../Moonpool/Task_local_storage/index.html | 8 +- moonpool/Moonpool/Trigger/index.html | 2 + .../For_runner_implementors/index.html | 4 +- moonpool/Moonpool/Ws_pool/index.html | 2 +- moonpool/Moonpool/index.html | 2 +- .../index.html | 2 +- moonpool/Moonpool__Trigger/index.html | 2 + moonpool/Moonpool__Types_/index.html | 2 + .../index.html | 2 +- moonpool/Moonpool_fib/Fls/index.html | 8 +- moonpool/Moonpool_private/Dla_/index.html | 2 - .../Thread_local_storage_/index.html | 2 - .../Moonpool_private/Ws_deque_/index.html | 2 +- moonpool/Moonpool_private/index.html | 2 +- .../index.html | 2 - moonpool/Moonpool_sync/Event/Infix/index.html | 2 + moonpool/Moonpool_sync/Event/index.html | 2 + moonpool/Moonpool_sync/Lock/index.html | 13 + moonpool/Moonpool_sync/index.html | 2 + moonpool/Moonpool_sync__/index.html | 2 + moonpool/Moonpool_sync__Event/index.html | 2 + moonpool/Moonpool_sync__Lock/index.html | 2 + moonpool/index.html | 2 +- .../Multicore_magic/Atomic_array/index.html | 2 + .../Transparent_atomic/index.html | 2 + multicore-magic/Multicore_magic/index.html | 36 + multicore-magic/Multicore_magic__/index.html | 2 + .../Multicore_magic__Cache/index.html | 2 + .../Multicore_magic__Index/index.html | 2 + .../Multicore_magic__Padding/index.html | 2 + .../index.html | 2 + multicore-magic/_doc-dir/CHANGES.md | 33 + multicore-magic/_doc-dir/LICENSE.md | 13 + multicore-magic/_doc-dir/README.md | 8 + multicore-magic/index.html | 2 + picos/Picos/Computation/Tx/index.html | 7 + picos/Picos/Computation/index.html | 47 ++ picos/Picos/Fiber/FLS/index.html | 2 + picos/Picos/Fiber/Maybe/index.html | 2 + picos/Picos/Fiber/index.html | 42 + picos/Picos/Handler/index.html | 7 + picos/Picos/Trigger/index.html | 18 + picos/Picos/index.html | 6 + picos/Picos__/index.html | 2 + picos/Picos__Intf/index.html | 2 + picos/Picos_domain/DLS/index.html | 2 + picos/Picos_domain/index.html | 2 + picos/Picos_thread/TLS/index.html | 2 + picos/Picos_thread/index.html | 2 + picos/_doc-dir/CHANGES.md | 171 ++++ picos/_doc-dir/LICENSE.md | 13 + picos/_doc-dir/README.md | 791 ++++++++++++++++++ picos/_doc-dir/odoc-pages/index.mld | 150 ++++ picos/index.html | 2 + picos_std/Picos_std_event/Event/index.html | 2 + picos_std/Picos_std_event/index.html | 2 + picos_std/Picos_std_event__/index.html | 2 + picos_std/Picos_std_event__Event/index.html | 2 + picos_std/Picos_std_finally/index.html | 71 ++ .../Picos_std_structured/Bundle/index.html | 6 + .../Picos_std_structured/Control/index.html | 17 + .../Picos_std_structured/Flock/index.html | 6 + .../Picos_std_structured/Promise/index.html | 2 + picos_std/Picos_std_structured/Run/index.html | 12 + picos_std/Picos_std_structured/index.html | 189 +++++ picos_std/Picos_std_structured__/index.html | 2 + .../Picos_std_structured__Bundle/index.html | 2 + .../Picos_std_structured__Control/index.html | 2 + .../Picos_std_structured__Flock/index.html | 2 + .../Picos_std_structured__Promise/index.html | 2 + .../Picos_std_structured__Run/index.html | 2 + picos_std/Picos_std_sync/Condition/index.html | 2 + picos_std/Picos_std_sync/Ivar/index.html | 2 + picos_std/Picos_std_sync/Latch/index.html | 4 + picos_std/Picos_std_sync/Lazy/index.html | 5 + picos_std/Picos_std_sync/Mutex/index.html | 2 + .../Semaphore/Binary/index.html | 2 + .../Semaphore/Counting/index.html | 2 + picos_std/Picos_std_sync/Semaphore/index.html | 2 + picos_std/Picos_std_sync/Stream/index.html | 2 + picos_std/Picos_std_sync/index.html | 98 +++ picos_std/Picos_std_sync__/index.html | 2 + .../Picos_std_sync__Condition/index.html | 2 + picos_std/Picos_std_sync__Ivar/index.html | 2 + picos_std/Picos_std_sync__Latch/index.html | 2 + picos_std/Picos_std_sync__Lazy/index.html | 2 + picos_std/Picos_std_sync__List_ext/index.html | 2 + picos_std/Picos_std_sync__Mutex/index.html | 2 + picos_std/Picos_std_sync__Q/index.html | 2 + .../Picos_std_sync__Semaphore/index.html | 2 + picos_std/Picos_std_sync__Stream/index.html | 2 + picos_std/_doc-dir/CHANGES.md | 171 ++++ picos_std/_doc-dir/LICENSE.md | 13 + picos_std/_doc-dir/README.md | 791 ++++++++++++++++++ picos_std/_doc-dir/odoc-pages/index.mld | 17 + picos_std/index.html | 2 + .../Thread_local_storage/index.html | 2 + .../Thread_local_storage__/index.html | 2 + .../Thread_local_storage__Atomic/index.html | 2 + thread-local-storage/_doc-dir/CHANGES.md | 11 + thread-local-storage/_doc-dir/LICENSE | 21 + thread-local-storage/_doc-dir/README.md | 10 + thread-local-storage/index.html | 2 + 118 files changed, 3059 insertions(+), 43 deletions(-) create mode 100644 backoff/Backoff/index.html create mode 100644 backoff/_doc-dir/CHANGES.md create mode 100644 backoff/_doc-dir/LICENSE.md create mode 100644 backoff/_doc-dir/README.md create mode 100644 backoff/index.html delete mode 100644 moonpool/Moonpool/Task_local_storage/Direct/index.html create mode 100644 moonpool/Moonpool/Trigger/index.html rename moonpool/{Moonpool__Suspend_ => Moonpool__Hmap_ls_}/index.html (74%) create mode 100644 moonpool/Moonpool__Trigger/index.html create mode 100644 moonpool/Moonpool__Types_/index.html rename moonpool/{Moonpool_private__Dla_ => Moonpool__Worker_loop_}/index.html (66%) delete mode 100644 moonpool/Moonpool_private/Dla_/index.html delete mode 100644 moonpool/Moonpool_private/Thread_local_storage_/index.html delete mode 100644 moonpool/Moonpool_private__Thread_local_storage_/index.html create mode 100644 moonpool/Moonpool_sync/Event/Infix/index.html create mode 100644 moonpool/Moonpool_sync/Event/index.html create mode 100644 moonpool/Moonpool_sync/Lock/index.html create mode 100644 moonpool/Moonpool_sync/index.html create mode 100644 moonpool/Moonpool_sync__/index.html create mode 100644 moonpool/Moonpool_sync__Event/index.html create mode 100644 moonpool/Moonpool_sync__Lock/index.html create mode 100644 multicore-magic/Multicore_magic/Atomic_array/index.html create mode 100644 multicore-magic/Multicore_magic/Transparent_atomic/index.html create mode 100644 multicore-magic/Multicore_magic/index.html create mode 100644 multicore-magic/Multicore_magic__/index.html create mode 100644 multicore-magic/Multicore_magic__Cache/index.html create mode 100644 multicore-magic/Multicore_magic__Index/index.html create mode 100644 multicore-magic/Multicore_magic__Padding/index.html create mode 100644 multicore-magic/Multicore_magic__Transparent_atomic/index.html create mode 100644 multicore-magic/_doc-dir/CHANGES.md create mode 100644 multicore-magic/_doc-dir/LICENSE.md create mode 100644 multicore-magic/_doc-dir/README.md create mode 100644 multicore-magic/index.html create mode 100644 picos/Picos/Computation/Tx/index.html create mode 100644 picos/Picos/Computation/index.html create mode 100644 picos/Picos/Fiber/FLS/index.html create mode 100644 picos/Picos/Fiber/Maybe/index.html create mode 100644 picos/Picos/Fiber/index.html create mode 100644 picos/Picos/Handler/index.html create mode 100644 picos/Picos/Trigger/index.html create mode 100644 picos/Picos/index.html create mode 100644 picos/Picos__/index.html create mode 100644 picos/Picos__Intf/index.html create mode 100644 picos/Picos_domain/DLS/index.html create mode 100644 picos/Picos_domain/index.html create mode 100644 picos/Picos_thread/TLS/index.html create mode 100644 picos/Picos_thread/index.html create mode 100644 picos/_doc-dir/CHANGES.md create mode 100644 picos/_doc-dir/LICENSE.md create mode 100644 picos/_doc-dir/README.md create mode 100644 picos/_doc-dir/odoc-pages/index.mld create mode 100644 picos/index.html create mode 100644 picos_std/Picos_std_event/Event/index.html create mode 100644 picos_std/Picos_std_event/index.html create mode 100644 picos_std/Picos_std_event__/index.html create mode 100644 picos_std/Picos_std_event__Event/index.html create mode 100644 picos_std/Picos_std_finally/index.html create mode 100644 picos_std/Picos_std_structured/Bundle/index.html create mode 100644 picos_std/Picos_std_structured/Control/index.html create mode 100644 picos_std/Picos_std_structured/Flock/index.html create mode 100644 picos_std/Picos_std_structured/Promise/index.html create mode 100644 picos_std/Picos_std_structured/Run/index.html create mode 100644 picos_std/Picos_std_structured/index.html create mode 100644 picos_std/Picos_std_structured__/index.html create mode 100644 picos_std/Picos_std_structured__Bundle/index.html create mode 100644 picos_std/Picos_std_structured__Control/index.html create mode 100644 picos_std/Picos_std_structured__Flock/index.html create mode 100644 picos_std/Picos_std_structured__Promise/index.html create mode 100644 picos_std/Picos_std_structured__Run/index.html create mode 100644 picos_std/Picos_std_sync/Condition/index.html create mode 100644 picos_std/Picos_std_sync/Ivar/index.html create mode 100644 picos_std/Picos_std_sync/Latch/index.html create mode 100644 picos_std/Picos_std_sync/Lazy/index.html create mode 100644 picos_std/Picos_std_sync/Mutex/index.html create mode 100644 picos_std/Picos_std_sync/Semaphore/Binary/index.html create mode 100644 picos_std/Picos_std_sync/Semaphore/Counting/index.html create mode 100644 picos_std/Picos_std_sync/Semaphore/index.html create mode 100644 picos_std/Picos_std_sync/Stream/index.html create mode 100644 picos_std/Picos_std_sync/index.html create mode 100644 picos_std/Picos_std_sync__/index.html create mode 100644 picos_std/Picos_std_sync__Condition/index.html create mode 100644 picos_std/Picos_std_sync__Ivar/index.html create mode 100644 picos_std/Picos_std_sync__Latch/index.html create mode 100644 picos_std/Picos_std_sync__Lazy/index.html create mode 100644 picos_std/Picos_std_sync__List_ext/index.html create mode 100644 picos_std/Picos_std_sync__Mutex/index.html create mode 100644 picos_std/Picos_std_sync__Q/index.html create mode 100644 picos_std/Picos_std_sync__Semaphore/index.html create mode 100644 picos_std/Picos_std_sync__Stream/index.html create mode 100644 picos_std/_doc-dir/CHANGES.md create mode 100644 picos_std/_doc-dir/LICENSE.md create mode 100644 picos_std/_doc-dir/README.md create mode 100644 picos_std/_doc-dir/odoc-pages/index.mld create mode 100644 picos_std/index.html create mode 100644 thread-local-storage/Thread_local_storage/index.html create mode 100644 thread-local-storage/Thread_local_storage__/index.html create mode 100644 thread-local-storage/Thread_local_storage__Atomic/index.html create mode 100644 thread-local-storage/_doc-dir/CHANGES.md create mode 100644 thread-local-storage/_doc-dir/LICENSE create mode 100644 thread-local-storage/_doc-dir/README.md create mode 100644 thread-local-storage/index.html diff --git a/backoff/Backoff/index.html b/backoff/Backoff/index.html new file mode 100644 index 00000000..374349d5 --- /dev/null +++ b/backoff/Backoff/index.html @@ -0,0 +1,2 @@ + +Backoff (backoff.Backoff)

Module Backoff

Randomized exponential backoff mechanism.

type t

Type of backoff values.

val max_wait_log : int

Logarithm of the maximum allowed value for wait.

val create : ?lower_wait_log:int -> ?upper_wait_log:int -> unit -> t

create creates a backoff value. upper_wait_log, lower_wait_log override the logarithmic upper and lower bound on the number of spins executed by once.

val default : t

default is equivalent to create ().

val once : t -> t

once b executes one random wait and returns a new backoff with logarithm of the current maximum value incremented unless it is already at upper_wait_log of b.

Note that this uses the default Stdlib Random per-domain generator.

val reset : t -> t

reset b returns a backoff equivalent to b except with current value set to the lower_wait_log of b.

val get_wait_log : t -> int

get_wait_log b returns logarithm of the maximum value of wait for next once.

diff --git a/backoff/_doc-dir/CHANGES.md b/backoff/_doc-dir/CHANGES.md new file mode 100644 index 00000000..02a66c93 --- /dev/null +++ b/backoff/_doc-dir/CHANGES.md @@ -0,0 +1,7 @@ +# Release notes + +All notable changes to this project will be documented in this file. + +## 0.1.0 + +- Initial version based on backoff from kcas (@lyrm, @polytypic) diff --git a/backoff/_doc-dir/LICENSE.md b/backoff/_doc-dir/LICENSE.md new file mode 100644 index 00000000..e107a366 --- /dev/null +++ b/backoff/_doc-dir/LICENSE.md @@ -0,0 +1,16 @@ +Copyright (c) 2015, Théo Laurent +Copyright (c) 2016, KC Sivaramakrishnan +Copyright (c) 2021, Sudha Parimala +Copyright (c) 2023, Vesa Karvonen + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/backoff/_doc-dir/README.md b/backoff/_doc-dir/README.md new file mode 100644 index 00000000..36d05f61 --- /dev/null +++ b/backoff/_doc-dir/README.md @@ -0,0 +1,94 @@ +[API reference](https://ocaml-multicore.github.io/backoff/doc/backoff/Backoff/index.html) + +# backoff - exponential backoff mechanism + +**backoff** provides an +[exponential backoff mechanism](https://en.wikipedia.org/wiki/Exponential_backoff) +[1]. It reduces contention by making a domain back off after failing an +operation contested by another domain, like acquiring a lock or performing a +`CAS` operation. + +## About contention + +Contention is what happens when multiple CPU cores try to access the same +location(s) in parallel. Let's take the example of multiple CPU cores trying to +perform a `CAS` on the same location at the same time. Only one is going to +success at each round of retries. By writing on a shared location, it +invalidates all other CPUs' caches. So at each round each CPU will have to read +the memory location again, leading to quadratic O(n²) bus traffic. + +## Exponential backoff + +Failing to access a shared resource means there is contention: some other CPU +cores are trying to access it at the same time. To avoid quadratic bus traffic, +the idea exploited by exponential backoff is to make each CPU core wait (spin) a +random bit before retrying. This way, they will try to access the resource at a +different time: that not only strongly decreases bus traffic but that also gets +them a better chance to get the resource, at they probably will compete for it +against less other CPU cores. Failing again probably means contention is high, +and they need to wait longer. In fact, each consecutive fail of a single CPU +core will make it wait twice longer (_exponential_ backoff !). + +Obviously, they cannot wait forever: there is an upper limit on the number of +times the initial waiting time can be doubled (see [Tuning](#tuning)), but +intuitively, a good waiting time should be at least around the time the +contested operation takes (in our example, the operation is a CAS) and at most a +few times that amount. + +## Tuning + +For better performance, backoff can be tuned. `Backoff.create` function has two +optional arguments for that: `upper_wait_log` and `lower_wait_log` that defines +the logarithmic upper and lower bound on the number of spins executed by +{!once}. + +## Drawbacks + +This mechanism has some drawbacks. First, it adds some delays: for example, when +a domain releases a contended lock, another domain, that has backed off after +failing acquiring it, will still have to finish its back-off loop before +retrying. Second, this increases any unfairness: any other thread that arrives +at that time or that has failed acquiring the lock for a lesser number of times +is more likely to acquire it as it will probably have a shorter waiting time. + +## Example + +To illustrate how to use backoff, here is a small implementation of +`test and test-and-set` spin lock [2]. + +```ocaml + type t = bool Atomic.t + + let create () = Atomic.make false + + let rec acquire ?(backoff = Backoff.detault) t = + if Atomic.get t then begin + Domain.cpu_relax (); + acquire ~backoff t + end + else if not (Atomic.compare_and_set t false true) then + acquire ~backoff:(Backoff.once backoff) t + + let release t = Atomic.set t false +``` + +This implementation can also be found [here](bench/taslock.ml), as well as a +small [benchmark](bench/test_tas.ml) to compare it to the same TAS lock but +without backoff. It can be launched with: + +```sh +dune exec ./bench/test_tas.exe > bench.data +``` + +and displayed (on linux) with: + +```sh +gnuplot -p -e 'plot for [col=2:4] "bench.data" using 1:col with lines title columnheader' +``` + +## References + +[1] Adaptive backoff synchronization techniques, A. Agarwal, M. Cherian (1989) + +[2] Dynamic Decentralized Cache Schemes for MIMD Parallel Processors, L.Rudolf, +Z.Segall (1984) diff --git a/backoff/index.html b/backoff/index.html new file mode 100644 index 00000000..e1cc53ff --- /dev/null +++ b/backoff/index.html @@ -0,0 +1,2 @@ + +index (backoff.index)

Package backoff

  • Backoff Randomized exponential backoff mechanism.

Package info

changes-files
license-files
readme-files
diff --git a/index.html b/index.html index fbf4d185..cb33e7af 100644 --- a/index.html +++ b/index.html @@ -1,2 +1,2 @@ -_opam

OCaml package documentation

Browse by name, by tag, the standard library and the OCaml manual (online, latest version).

Generated for /home/runner/work/moonpool/moonpool/_opam/lib

Packages by name

Packages by tag

\ No newline at end of file +_opam

OCaml package documentation

Browse by name, by tag, the standard library and the OCaml manual (online, latest version).

Generated for /home/runner/work/moonpool/moonpool/_opam/lib

\ No newline at end of file diff --git a/moonpool/Moonpool/Background_thread/For_runner_implementors/index.html b/moonpool/Moonpool/Background_thread/For_runner_implementors/index.html index 628ac157..ae1c770b 100644 --- a/moonpool/Moonpool/Background_thread/For_runner_implementors/index.html +++ b/moonpool/Moonpool/Background_thread/For_runner_implementors/index.html @@ -3,6 +3,6 @@ size:(unit -> int) -> num_tasks:(unit -> int) -> shutdown:(wait:bool -> unit -> unit) -> - run_async:(ls:Task_local_storage.t -> task -> unit) -> + run_async:(fiber:fiber -> task -> unit) -> unit -> - t

Create a new runner.

NOTE: the runner should support DLA and Suspend_ on OCaml 5.x, so that Fork_join and other 5.x features work properly.

Key that should be used by each runner to store itself in TLS on every thread it controls, so that tasks running on these threads can access the runner. This is necessary for get_current_runner to work.

+ t

Create a new runner.

NOTE: the runner should support DLA and Suspend_ on OCaml 5.x, so that Fork_join and other 5.x features work properly.

val k_cur_runner : t Thread_local_storage.t

Key that should be used by each runner to store itself in TLS on every thread it controls, so that tasks running on these threads can access the runner. This is necessary for get_current_runner to work.

diff --git a/moonpool/Moonpool/Background_thread/index.html b/moonpool/Moonpool/Background_thread/index.html index 2ab43178..c8d93c6b 100644 --- a/moonpool/Moonpool/Background_thread/index.html +++ b/moonpool/Moonpool/Background_thread/index.html @@ -1,5 +1,5 @@ -Background_thread (moonpool.Moonpool.Background_thread)

Module Moonpool.Background_thread

A simple runner with a single background thread.

Because this is guaranteed to have a single worker thread, tasks scheduled in this runner always run asynchronously but in a sequential fashion.

This is similar to Fifo_pool with exactly one thread.

  • since 0.6
include module type of Runner
type task = unit -> unit
type t

A runner.

If a runner is no longer needed, shutdown can be used to signal all worker threads in it to stop (after they finish their work), and wait for them to stop.

The threads are distributed across a fixed domain pool (whose size is determined by Domain.recommended_domain_count on OCaml 5, and simple the single runtime on OCaml 4).

val size : t -> int

Number of threads/workers.

val num_tasks : t -> int

Current number of tasks. This is at best a snapshot, useful for metrics and debugging.

val shutdown : t -> unit

Shutdown the runner and wait for it to terminate. Idempotent.

val shutdown_without_waiting : t -> unit

Shutdown the pool, and do not wait for it to terminate. Idempotent.

exception Shutdown
val run_async : ?ls:Task_local_storage.t -> t -> task -> unit

run_async pool f schedules f for later execution on the runner in one of the threads. f() will run on one of the runner's worker threads/domains.

  • parameter ls

    if provided, run the task with this initial local storage

  • raises Shutdown

    if the runner was shut down before run_async was called.

val run_wait_block : ?ls:Task_local_storage.t -> t -> (unit -> 'a) -> 'a

run_wait_block pool f schedules f for later execution on the pool, like run_async. It then blocks the current thread until f() is done executing, and returns its result. If f() raises an exception, then run_wait_block pool f will raise it as well.

NOTE be careful with deadlocks (see notes in Fut.wait_block about the required discipline to avoid deadlocks).

  • raises Shutdown

    if the runner was already shut down

val dummy : t

Runner that fails when scheduling tasks on it. Calling run_async on it will raise Failure.

  • since 0.6

Implementing runners

module For_runner_implementors : sig ... end

This module is specifically intended for users who implement their own runners. Regular users of Moonpool should not need to look at it.

val get_current_runner : unit -> t option

Access the current runner. This returns Some r if the call happens on a thread that belongs in a runner.

  • since 0.5
val get_current_storage : unit -> Task_local_storage.t option

get_current_storage runner gets the local storage for the currently running task.

type ('a, 'b) create_args = +Background_thread (moonpool.Moonpool.Background_thread)

Module Moonpool.Background_thread

A simple runner with a single background thread.

Because this is guaranteed to have a single worker thread, tasks scheduled in this runner always run asynchronously but in a sequential fashion.

This is similar to Fifo_pool with exactly one thread.

  • since 0.6
include module type of Runner
type fiber = Picos.Fiber.t
type task = unit -> unit
type t

A runner.

If a runner is no longer needed, shutdown can be used to signal all worker threads in it to stop (after they finish their work), and wait for them to stop.

The threads are distributed across a fixed domain pool (whose size is determined by Domain.recommended_domain_count on OCaml 5, and simple the single runtime on OCaml 4).

val size : t -> int

Number of threads/workers.

val num_tasks : t -> int

Current number of tasks. This is at best a snapshot, useful for metrics and debugging.

val shutdown : t -> unit

Shutdown the runner and wait for it to terminate. Idempotent.

val shutdown_without_waiting : t -> unit

Shutdown the pool, and do not wait for it to terminate. Idempotent.

exception Shutdown
val run_async : ?fiber:fiber -> t -> task -> unit

run_async pool f schedules f for later execution on the runner in one of the threads. f() will run on one of the runner's worker threads/domains.

  • parameter fiber

    if provided, run the task with this initial fiber data

  • raises Shutdown

    if the runner was shut down before run_async was called.

val run_wait_block : ?fiber:fiber -> t -> (unit -> 'a) -> 'a

run_wait_block pool f schedules f for later execution on the pool, like run_async. It then blocks the current thread until f() is done executing, and returns its result. If f() raises an exception, then run_wait_block pool f will raise it as well.

NOTE be careful with deadlocks (see notes in Fut.wait_block about the required discipline to avoid deadlocks).

  • raises Shutdown

    if the runner was already shut down

val dummy : t

Runner that fails when scheduling tasks on it. Calling run_async on it will raise Failure.

  • since 0.6

Implementing runners

module For_runner_implementors : sig ... end

This module is specifically intended for users who implement their own runners. Regular users of Moonpool should not need to look at it.

val get_current_runner : unit -> t option

Access the current runner. This returns Some r if the call happens on a thread that belongs in a runner.

  • since 0.5
val get_current_fiber : unit -> fiber option

get_current_storage runner gets the local storage for the currently running task.

type ('a, 'b) create_args = ?on_init_thread:(dom_id:int -> t_id:int -> unit -> unit) -> ?on_exit_thread:(dom_id:int -> t_id:int -> unit -> unit) -> ?on_exn:(exn -> Stdlib.Printexc.raw_backtrace -> unit) -> diff --git a/moonpool/Moonpool/Exn_bt/index.html b/moonpool/Moonpool/Exn_bt/index.html index a349985a..d7489959 100644 --- a/moonpool/Moonpool/Exn_bt/index.html +++ b/moonpool/Moonpool/Exn_bt/index.html @@ -1,2 +1,2 @@ -Exn_bt (moonpool.Moonpool.Exn_bt)

Module Moonpool.Exn_bt

Exception with backtrace.

  • since 0.6

An exception bundled with a backtrace

val exn : t -> exn
val make : exn -> Stdlib.Printexc.raw_backtrace -> t

Trivial builder

val get : exn -> t

get exn is make exn (get_raw_backtrace ())

val get_callstack : int -> exn -> t
val raise : t -> 'a

Raise the exception with its save backtrace

val show : t -> string

Simple printing

val pp : Stdlib.Format.formatter -> t -> unit
type nonrec 'a result = ('a, t) result
+Exn_bt (moonpool.Moonpool.Exn_bt)

Module Moonpool.Exn_bt

Exception with backtrace.

Type changed

  • since NEXT_RELEASE
  • since 0.6

An exception bundled with a backtrace

val exn : t -> exn
val raise : t -> 'a
val get : exn -> t
val get_callstack : int -> exn -> t
val make : exn -> Stdlib.Printexc.raw_backtrace -> t

Trivial builder

val show : t -> string

Simple printing

val pp : Stdlib.Format.formatter -> t -> unit
type nonrec 'a result = ('a, t) result
val unwrap : 'a result -> 'a

unwrap (Ok x) is x, unwrap (Error ebt) re-raises ebt.

  • since NEXT_RELEASE
diff --git a/moonpool/Moonpool/Fifo_pool/For_runner_implementors/index.html b/moonpool/Moonpool/Fifo_pool/For_runner_implementors/index.html index c50b0dc7..1dcd6f25 100644 --- a/moonpool/Moonpool/Fifo_pool/For_runner_implementors/index.html +++ b/moonpool/Moonpool/Fifo_pool/For_runner_implementors/index.html @@ -3,6 +3,6 @@ size:(unit -> int) -> num_tasks:(unit -> int) -> shutdown:(wait:bool -> unit -> unit) -> - run_async:(ls:Task_local_storage.t -> task -> unit) -> + run_async:(fiber:fiber -> task -> unit) -> unit -> - t

Create a new runner.

NOTE: the runner should support DLA and Suspend_ on OCaml 5.x, so that Fork_join and other 5.x features work properly.

Key that should be used by each runner to store itself in TLS on every thread it controls, so that tasks running on these threads can access the runner. This is necessary for get_current_runner to work.

+ t

Create a new runner.

NOTE: the runner should support DLA and Suspend_ on OCaml 5.x, so that Fork_join and other 5.x features work properly.

val k_cur_runner : t Thread_local_storage.t

Key that should be used by each runner to store itself in TLS on every thread it controls, so that tasks running on these threads can access the runner. This is necessary for get_current_runner to work.

diff --git a/moonpool/Moonpool/Fifo_pool/index.html b/moonpool/Moonpool/Fifo_pool/index.html index 05197745..9e9fccef 100644 --- a/moonpool/Moonpool/Fifo_pool/index.html +++ b/moonpool/Moonpool/Fifo_pool/index.html @@ -1,5 +1,5 @@ -Fifo_pool (moonpool.Moonpool.Fifo_pool)

Module Moonpool.Fifo_pool

A simple thread pool in FIFO order.

FIFO: first-in, first-out. Basically tasks are put into a queue, and worker threads pull them out of the queue at the other end.

Since this uses a single blocking queue to manage tasks, it's very simple and reliable. The number of worker threads is fixed, but they are spread over several domains to enable parallelism.

This can be useful for latency-sensitive applications (e.g. as a pool of workers for network servers). Work-stealing pools might have higher throughput but they're very unfair to some tasks; by contrast, here, older tasks have priority over younger tasks.

  • since 0.5
include module type of Runner
type task = unit -> unit
type t

A runner.

If a runner is no longer needed, shutdown can be used to signal all worker threads in it to stop (after they finish their work), and wait for them to stop.

The threads are distributed across a fixed domain pool (whose size is determined by Domain.recommended_domain_count on OCaml 5, and simple the single runtime on OCaml 4).

val size : t -> int

Number of threads/workers.

val num_tasks : t -> int

Current number of tasks. This is at best a snapshot, useful for metrics and debugging.

val shutdown : t -> unit

Shutdown the runner and wait for it to terminate. Idempotent.

val shutdown_without_waiting : t -> unit

Shutdown the pool, and do not wait for it to terminate. Idempotent.

exception Shutdown
val run_async : ?ls:Task_local_storage.t -> t -> task -> unit

run_async pool f schedules f for later execution on the runner in one of the threads. f() will run on one of the runner's worker threads/domains.

  • parameter ls

    if provided, run the task with this initial local storage

  • raises Shutdown

    if the runner was shut down before run_async was called.

val run_wait_block : ?ls:Task_local_storage.t -> t -> (unit -> 'a) -> 'a

run_wait_block pool f schedules f for later execution on the pool, like run_async. It then blocks the current thread until f() is done executing, and returns its result. If f() raises an exception, then run_wait_block pool f will raise it as well.

NOTE be careful with deadlocks (see notes in Fut.wait_block about the required discipline to avoid deadlocks).

  • raises Shutdown

    if the runner was already shut down

val dummy : t

Runner that fails when scheduling tasks on it. Calling run_async on it will raise Failure.

  • since 0.6

Implementing runners

module For_runner_implementors : sig ... end

This module is specifically intended for users who implement their own runners. Regular users of Moonpool should not need to look at it.

val get_current_runner : unit -> t option

Access the current runner. This returns Some r if the call happens on a thread that belongs in a runner.

  • since 0.5
val get_current_storage : unit -> Task_local_storage.t option

get_current_storage runner gets the local storage for the currently running task.

type ('a, 'b) create_args = +Fifo_pool (moonpool.Moonpool.Fifo_pool)

Module Moonpool.Fifo_pool

A simple thread pool in FIFO order.

FIFO: first-in, first-out. Basically tasks are put into a queue, and worker threads pull them out of the queue at the other end.

Since this uses a single blocking queue to manage tasks, it's very simple and reliable. The number of worker threads is fixed, but they are spread over several domains to enable parallelism.

This can be useful for latency-sensitive applications (e.g. as a pool of workers for network servers). Work-stealing pools might have higher throughput but they're very unfair to some tasks; by contrast, here, older tasks have priority over younger tasks.

  • since 0.5
include module type of Runner
type fiber = Picos.Fiber.t
type task = unit -> unit
type t

A runner.

If a runner is no longer needed, shutdown can be used to signal all worker threads in it to stop (after they finish their work), and wait for them to stop.

The threads are distributed across a fixed domain pool (whose size is determined by Domain.recommended_domain_count on OCaml 5, and simple the single runtime on OCaml 4).

val size : t -> int

Number of threads/workers.

val num_tasks : t -> int

Current number of tasks. This is at best a snapshot, useful for metrics and debugging.

val shutdown : t -> unit

Shutdown the runner and wait for it to terminate. Idempotent.

val shutdown_without_waiting : t -> unit

Shutdown the pool, and do not wait for it to terminate. Idempotent.

exception Shutdown
val run_async : ?fiber:fiber -> t -> task -> unit

run_async pool f schedules f for later execution on the runner in one of the threads. f() will run on one of the runner's worker threads/domains.

  • parameter fiber

    if provided, run the task with this initial fiber data

  • raises Shutdown

    if the runner was shut down before run_async was called.

val run_wait_block : ?fiber:fiber -> t -> (unit -> 'a) -> 'a

run_wait_block pool f schedules f for later execution on the pool, like run_async. It then blocks the current thread until f() is done executing, and returns its result. If f() raises an exception, then run_wait_block pool f will raise it as well.

NOTE be careful with deadlocks (see notes in Fut.wait_block about the required discipline to avoid deadlocks).

  • raises Shutdown

    if the runner was already shut down

val dummy : t

Runner that fails when scheduling tasks on it. Calling run_async on it will raise Failure.

  • since 0.6

Implementing runners

module For_runner_implementors : sig ... end

This module is specifically intended for users who implement their own runners. Regular users of Moonpool should not need to look at it.

val get_current_runner : unit -> t option

Access the current runner. This returns Some r if the call happens on a thread that belongs in a runner.

  • since 0.5
val get_current_fiber : unit -> fiber option

get_current_storage runner gets the local storage for the currently running task.

type ('a, 'b) create_args = ?on_init_thread:(dom_id:int -> t_id:int -> unit -> unit) -> ?on_exit_thread:(dom_id:int -> t_id:int -> unit -> unit) -> ?on_exn:(exn -> Stdlib.Printexc.raw_backtrace -> unit) -> diff --git a/moonpool/Moonpool/Fut/index.html b/moonpool/Moonpool/Fut/index.html index 3f337048..fbccf56e 100644 --- a/moonpool/Moonpool/Fut/index.html +++ b/moonpool/Moonpool/Fut/index.html @@ -1,2 +1,2 @@ -Fut (moonpool.Moonpool.Fut)

Module Moonpool.Fut

Futures.

A future of type 'a t represents the result of a computation that will yield a value of type 'a.

Typically, the computation is running on a thread pool Runner.t and will proceed on some worker. Once set, a future cannot change. It either succeeds (storing a Ok x with x: 'a), or fail (storing a Error (exn, bt) with an exception and the corresponding backtrace).

Combinators such as map and join_array can be used to produce futures from other futures (in a monadic way). Some combinators take a on argument to specify a runner on which the intermediate computation takes place; for example map ~on:pool ~f fut maps the value in fut using function f, applicatively; the call to f happens on the runner pool (once fut resolves successfully with a value).

type 'a or_error = ('a, Exn_bt.t) result
type 'a t

A future with a result of type 'a.

type 'a promise

A promise, which can be fulfilled exactly once to set the corresponding future

val make : unit -> 'a t * 'a promise

Make a new future with the associated promise.

val on_result : 'a t -> ('a or_error -> unit) -> unit

on_result fut f registers f to be called in the future when fut is set ; or calls f immediately if fut is already set.

exception Already_fulfilled
val fulfill : 'a promise -> 'a or_error -> unit

Fullfill the promise, setting the future at the same time.

val fulfill_idempotent : 'a promise -> 'a or_error -> unit

Fullfill the promise, setting the future at the same time. Does nothing if the promise is already fulfilled.

val return : 'a -> 'a t

Already settled future, with a result

val fail : exn -> Stdlib.Printexc.raw_backtrace -> _ t

Already settled future, with a failure

val fail_exn_bt : Exn_bt.t -> _ t

Fail from a bundle of exception and backtrace

  • since 0.6
val of_result : 'a or_error -> 'a t
val is_resolved : _ t -> bool

is_resolved fut is true iff fut is resolved.

val peek : 'a t -> 'a or_error option

peek fut returns Some r if fut is currently resolved with r, and None if fut is not resolved yet.

exception Not_ready
  • since 0.2
val get_or_fail : 'a t -> 'a or_error

get_or_fail fut obtains the result from fut if it's fulfilled (i.e. if peek fut returns Some res, get_or_fail fut returns res).

  • since 0.2
val get_or_fail_exn : 'a t -> 'a

get_or_fail_exn fut obtains the result from fut if it's fulfilled, like get_or_fail. If the result is an Error _, the exception inside is re-raised.

  • since 0.2
val is_done : _ t -> bool

Is the future resolved? This is the same as peek fut |> Option.is_some.

  • since 0.2
val is_success : _ t -> bool

Checks if the future is resolved with Ok _ as a result.

  • since 0.6
val is_failed : _ t -> bool

Checks if the future is resolved with Error _ as a result.

  • since 0.6
val raise_if_failed : _ t -> unit

raise_if_failed fut raises e if fut failed with e.

  • since 0.6

Combinators

val spawn : on:Runner.t -> (unit -> 'a) -> 'a t

spaw ~on f runs f() on the given runner on, and return a future that will hold its result.

val spawn_on_current_runner : (unit -> 'a) -> 'a t

This must be run from inside a runner, and schedules the new task on it as well.

See Runner.get_current_runner to see how the runner is found.

  • since 0.5
  • raises Failure

    if run from outside a runner.

val reify_error : 'a t -> 'a or_error t

reify_error fut turns a failing future into a non-failing one that contain Error (exn, bt). A non-failing future returning x is turned into Ok x

  • since 0.4
val map : ?on:Runner.t -> f:('a -> 'b) -> 'a t -> 'b t

map ?on ~f fut returns a new future fut2 that resolves with f x if fut resolved with x; and fails with e if fut fails with e or f x raises e.

  • parameter on

    if provided, f runs on the given runner

val bind : ?on:Runner.t -> f:('a -> 'b t) -> 'a t -> 'b t

bind ?on ~f fut returns a new future fut2 that resolves like the future f x if fut resolved with x; and fails with e if fut fails with e or f x raises e.

  • parameter on

    if provided, f runs on the given runner

val bind_reify_error : ?on:Runner.t -> f:('a or_error -> 'b t) -> 'a t -> 'b t

bind_reify_error ?on ~f fut returns a new future fut2 that resolves like the future f (Ok x) if fut resolved with x; and resolves like the future f (Error (exn, bt)) if fut fails with exn and backtrace bt.

  • parameter on

    if provided, f runs on the given runner

  • since 0.4
val join : 'a t t -> 'a t

join fut is fut >>= Fun.id. It joins the inner layer of the future.

  • since 0.2
val both : 'a t -> 'b t -> ('a * 'b) t

both a b succeeds with x, y if a succeeds with x and b succeeds with y, or fails if any of them fails.

val choose : 'a t -> 'b t -> ('a, 'b) Either.t t

choose a b succeeds Left x or Right y if a succeeds with x or b succeeds with y, or fails if both of them fails. If they both succeed, it is not specified which result is used.

val choose_same : 'a t -> 'a t -> 'a t

choose_same a b succeeds with the value of one of a or b if they succeed, or fails if both fail. If they both succeed, it is not specified which result is used.

val join_array : 'a t array -> 'a array t

Wait for all the futures in the array. Fails if any future fails.

val join_list : 'a t list -> 'a list t

Wait for all the futures in the list. Fails if any future fails.

module Advanced : sig ... end
val map_list : f:('a -> 'b t) -> 'a list -> 'b list t

map_list ~f l is like join_list @@ List.map f l.

  • since 0.5.1
val wait_array : _ t array -> unit t

wait_array arr waits for all futures in arr to resolve. It discards the individual results of futures in arr. It fails if any future fails.

val wait_list : _ t list -> unit t

wait_list l waits for all futures in l to resolve. It discards the individual results of futures in l. It fails if any future fails.

val for_ : on:Runner.t -> int -> (int -> unit) -> unit t

for_ ~on n f runs f 0, f 1, …, f (n-1) on the runner, and returns a future that resolves when all the tasks have resolved, or fails as soon as one task has failed.

val for_array : on:Runner.t -> 'a array -> (int -> 'a -> unit) -> unit t

for_array ~on arr f runs f 0 arr.(0), …, f (n-1) arr.(n-1) in the runner (where n = Array.length arr), and returns a future that resolves when all the tasks are done, or fails if any of them fails.

  • since 0.2
val for_list : on:Runner.t -> 'a list -> ('a -> unit) -> unit t

for_list ~on l f is like for_array ~on (Array.of_list l) f.

  • since 0.2

Await

NOTE This is only available on OCaml 5.

val await : 'a t -> 'a

await fut suspends the current tasks until fut is fulfilled, then resumes the task on this same runner (but possibly on a different thread/domain).

  • since 0.3

This must only be run from inside the runner itself. The runner must support Suspend_. NOTE: only on OCaml 5.x

Blocking

val wait_block : 'a t -> 'a or_error

wait_block fut blocks the current thread until fut is resolved, and returns its value.

NOTE: A word of warning: this will monopolize the calling thread until the future resolves. This can also easily cause deadlocks, if enough threads in a pool call wait_block on futures running on the same pool or a pool depending on it.

A good rule to avoid deadlocks is to run this from outside of any pool, or to have an acyclic order between pools where wait_block is only called from a pool on futures evaluated in a pool that comes lower in the hierarchy. If this rule is broken, it is possible for all threads in a pool to wait for futures that can only make progress on these same threads, hence the deadlock.

val wait_block_exn : 'a t -> 'a

Same as wait_block but re-raises the exception if the future failed.

Infix operators

These combinators run on either the current pool (if present), or on the same thread that just fulfilled the previous future if not.

They were previously present as module Infix_local and val infix, but are now simplified.

  • since 0.5
module Infix : sig ... end
include module type of Infix
  • since 0.5
val (>|=) : 'a t -> ('a -> 'b) -> 'b t
val (>>=) : 'a t -> ('a -> 'b t) -> 'b t
val let+ : 'a t -> ('a -> 'b) -> 'b t
val and+ : 'a t -> 'b t -> ('a * 'b) t
val let* : 'a t -> ('a -> 'b t) -> 'b t
val and* : 'a t -> 'b t -> ('a * 'b) t
module Infix_local = Infix
+Fut (moonpool.Moonpool.Fut)

Module Moonpool.Fut

Futures.

A future of type 'a t represents the result of a computation that will yield a value of type 'a.

Typically, the computation is running on a thread pool Runner.t and will proceed on some worker. Once set, a future cannot change. It either succeeds (storing a Ok x with x: 'a), or fail (storing a Error (exn, bt) with an exception and the corresponding backtrace).

Combinators such as map and join_array can be used to produce futures from other futures (in a monadic way). Some combinators take a on argument to specify a runner on which the intermediate computation takes place; for example map ~on:pool ~f fut maps the value in fut using function f, applicatively; the call to f happens on the runner pool (once fut resolves successfully with a value).

type 'a or_error = ('a, Exn_bt.t) result
type 'a t

A future with a result of type 'a.

type 'a promise

A promise, which can be fulfilled exactly once to set the corresponding future

val make : unit -> 'a t * 'a promise

Make a new future with the associated promise.

val on_result : 'a t -> ('a or_error -> unit) -> unit

on_result fut f registers f to be called in the future when fut is set ; or calls f immediately if fut is already set.

val on_result_ignore : _ t -> (Exn_bt.t option -> unit) -> unit

on_result_ignore fut f registers f to be called in the future when fut is set; or calls f immediately if fut is already set. It does not pass the result, only a success/error signal.

  • since NEXT_RELEASE
exception Already_fulfilled
val fulfill : 'a promise -> 'a or_error -> unit

Fullfill the promise, setting the future at the same time.

val fulfill_idempotent : 'a promise -> 'a or_error -> unit

Fullfill the promise, setting the future at the same time. Does nothing if the promise is already fulfilled.

val return : 'a -> 'a t

Already settled future, with a result

val fail : exn -> Stdlib.Printexc.raw_backtrace -> _ t

Already settled future, with a failure

val fail_exn_bt : Exn_bt.t -> _ t

Fail from a bundle of exception and backtrace

  • since 0.6
val of_result : 'a or_error -> 'a t
val is_resolved : _ t -> bool

is_resolved fut is true iff fut is resolved.

val peek : 'a t -> 'a or_error option

peek fut returns Some r if fut is currently resolved with r, and None if fut is not resolved yet.

exception Not_ready
  • since 0.2
val get_or_fail : 'a t -> 'a or_error

get_or_fail fut obtains the result from fut if it's fulfilled (i.e. if peek fut returns Some res, get_or_fail fut returns res).

  • since 0.2
val get_or_fail_exn : 'a t -> 'a

get_or_fail_exn fut obtains the result from fut if it's fulfilled, like get_or_fail. If the result is an Error _, the exception inside is re-raised.

  • since 0.2
val is_done : _ t -> bool

Is the future resolved? This is the same as peek fut |> Option.is_some.

  • since 0.2
val is_success : _ t -> bool

Checks if the future is resolved with Ok _ as a result.

  • since 0.6
val is_failed : _ t -> bool

Checks if the future is resolved with Error _ as a result.

  • since 0.6
val raise_if_failed : _ t -> unit

raise_if_failed fut raises e if fut failed with e.

  • since 0.6

Combinators

val spawn : on:Runner.t -> (unit -> 'a) -> 'a t

spaw ~on f runs f() on the given runner on, and return a future that will hold its result.

val spawn_on_current_runner : (unit -> 'a) -> 'a t

This must be run from inside a runner, and schedules the new task on it as well.

See Runner.get_current_runner to see how the runner is found.

  • since 0.5
  • raises Failure

    if run from outside a runner.

val reify_error : 'a t -> 'a or_error t

reify_error fut turns a failing future into a non-failing one that contain Error (exn, bt). A non-failing future returning x is turned into Ok x

  • since 0.4
val map : ?on:Runner.t -> f:('a -> 'b) -> 'a t -> 'b t

map ?on ~f fut returns a new future fut2 that resolves with f x if fut resolved with x; and fails with e if fut fails with e or f x raises e.

  • parameter on

    if provided, f runs on the given runner

val bind : ?on:Runner.t -> f:('a -> 'b t) -> 'a t -> 'b t

bind ?on ~f fut returns a new future fut2 that resolves like the future f x if fut resolved with x; and fails with e if fut fails with e or f x raises e.

  • parameter on

    if provided, f runs on the given runner

val bind_reify_error : ?on:Runner.t -> f:('a or_error -> 'b t) -> 'a t -> 'b t

bind_reify_error ?on ~f fut returns a new future fut2 that resolves like the future f (Ok x) if fut resolved with x; and resolves like the future f (Error (exn, bt)) if fut fails with exn and backtrace bt.

  • parameter on

    if provided, f runs on the given runner

  • since 0.4
val join : 'a t t -> 'a t

join fut is fut >>= Fun.id. It joins the inner layer of the future.

  • since 0.2
val both : 'a t -> 'b t -> ('a * 'b) t

both a b succeeds with x, y if a succeeds with x and b succeeds with y, or fails if any of them fails.

val choose : 'a t -> 'b t -> ('a, 'b) Either.t t

choose a b succeeds Left x or Right y if a succeeds with x or b succeeds with y, or fails if both of them fails. If they both succeed, it is not specified which result is used.

val choose_same : 'a t -> 'a t -> 'a t

choose_same a b succeeds with the value of one of a or b if they succeed, or fails if both fail. If they both succeed, it is not specified which result is used.

val join_array : 'a t array -> 'a array t

Wait for all the futures in the array. Fails if any future fails.

val join_list : 'a t list -> 'a list t

Wait for all the futures in the list. Fails if any future fails.

module Advanced : sig ... end
val map_list : f:('a -> 'b t) -> 'a list -> 'b list t

map_list ~f l is like join_list @@ List.map f l.

  • since 0.5.1
val wait_array : _ t array -> unit t

wait_array arr waits for all futures in arr to resolve. It discards the individual results of futures in arr. It fails if any future fails.

val wait_list : _ t list -> unit t

wait_list l waits for all futures in l to resolve. It discards the individual results of futures in l. It fails if any future fails.

val for_ : on:Runner.t -> int -> (int -> unit) -> unit t

for_ ~on n f runs f 0, f 1, …, f (n-1) on the runner, and returns a future that resolves when all the tasks have resolved, or fails as soon as one task has failed.

val for_array : on:Runner.t -> 'a array -> (int -> 'a -> unit) -> unit t

for_array ~on arr f runs f 0 arr.(0), …, f (n-1) arr.(n-1) in the runner (where n = Array.length arr), and returns a future that resolves when all the tasks are done, or fails if any of them fails.

  • since 0.2
val for_list : on:Runner.t -> 'a list -> ('a -> unit) -> unit t

for_list ~on l f is like for_array ~on (Array.of_list l) f.

  • since 0.2

Await

NOTE This is only available on OCaml 5.

val await : 'a t -> 'a

await fut suspends the current tasks until fut is fulfilled, then resumes the task on this same runner (but possibly on a different thread/domain).

  • since 0.3

This must only be run from inside the runner itself. The runner must support Suspend_. NOTE: only on OCaml 5.x

Blocking

val wait_block : 'a t -> 'a or_error

wait_block fut blocks the current thread until fut is resolved, and returns its value.

NOTE: A word of warning: this will monopolize the calling thread until the future resolves. This can also easily cause deadlocks, if enough threads in a pool call wait_block on futures running on the same pool or a pool depending on it.

A good rule to avoid deadlocks is to run this from outside of any pool, or to have an acyclic order between pools where wait_block is only called from a pool on futures evaluated in a pool that comes lower in the hierarchy. If this rule is broken, it is possible for all threads in a pool to wait for futures that can only make progress on these same threads, hence the deadlock.

val wait_block_exn : 'a t -> 'a

Same as wait_block but re-raises the exception if the future failed.

Infix operators

These combinators run on either the current pool (if present), or on the same thread that just fulfilled the previous future if not.

They were previously present as module Infix_local and val infix, but are now simplified.

  • since 0.5
module Infix : sig ... end
include module type of Infix
  • since 0.5
val (>|=) : 'a t -> ('a -> 'b) -> 'b t
val (>>=) : 'a t -> ('a -> 'b t) -> 'b t
val let+ : 'a t -> ('a -> 'b) -> 'b t
val and+ : 'a t -> 'b t -> ('a * 'b) t
val let* : 'a t -> ('a -> 'b t) -> 'b t
val and* : 'a t -> 'b t -> ('a * 'b) t
module Infix_local = Infix
diff --git a/moonpool/Moonpool/Runner/For_runner_implementors/index.html b/moonpool/Moonpool/Runner/For_runner_implementors/index.html index 7ebc405c..dcff72b9 100644 --- a/moonpool/Moonpool/Runner/For_runner_implementors/index.html +++ b/moonpool/Moonpool/Runner/For_runner_implementors/index.html @@ -3,6 +3,6 @@ size:(unit -> int) -> num_tasks:(unit -> int) -> shutdown:(wait:bool -> unit -> unit) -> - run_async:(ls:Task_local_storage.t -> task -> unit) -> + run_async:(fiber:fiber -> task -> unit) -> unit -> - t

Create a new runner.

NOTE: the runner should support DLA and Suspend_ on OCaml 5.x, so that Fork_join and other 5.x features work properly.

Key that should be used by each runner to store itself in TLS on every thread it controls, so that tasks running on these threads can access the runner. This is necessary for get_current_runner to work.

+ t

Create a new runner.

NOTE: the runner should support DLA and Suspend_ on OCaml 5.x, so that Fork_join and other 5.x features work properly.

val k_cur_runner : t Thread_local_storage.t

Key that should be used by each runner to store itself in TLS on every thread it controls, so that tasks running on these threads can access the runner. This is necessary for get_current_runner to work.

diff --git a/moonpool/Moonpool/Runner/index.html b/moonpool/Moonpool/Runner/index.html index 9222d0d9..f3821683 100644 --- a/moonpool/Moonpool/Runner/index.html +++ b/moonpool/Moonpool/Runner/index.html @@ -1,2 +1,2 @@ -Runner (moonpool.Moonpool.Runner)

Module Moonpool.Runner

Interface for runners.

This provides an abstraction for running tasks in the background, which is implemented by various thread pools.

  • since 0.3
type task = unit -> unit
type t

A runner.

If a runner is no longer needed, shutdown can be used to signal all worker threads in it to stop (after they finish their work), and wait for them to stop.

The threads are distributed across a fixed domain pool (whose size is determined by Domain.recommended_domain_count on OCaml 5, and simple the single runtime on OCaml 4).

val size : t -> int

Number of threads/workers.

val num_tasks : t -> int

Current number of tasks. This is at best a snapshot, useful for metrics and debugging.

val shutdown : t -> unit

Shutdown the runner and wait for it to terminate. Idempotent.

val shutdown_without_waiting : t -> unit

Shutdown the pool, and do not wait for it to terminate. Idempotent.

exception Shutdown
val run_async : ?ls:Task_local_storage.t -> t -> task -> unit

run_async pool f schedules f for later execution on the runner in one of the threads. f() will run on one of the runner's worker threads/domains.

  • parameter ls

    if provided, run the task with this initial local storage

  • raises Shutdown

    if the runner was shut down before run_async was called.

val run_wait_block : ?ls:Task_local_storage.t -> t -> (unit -> 'a) -> 'a

run_wait_block pool f schedules f for later execution on the pool, like run_async. It then blocks the current thread until f() is done executing, and returns its result. If f() raises an exception, then run_wait_block pool f will raise it as well.

NOTE be careful with deadlocks (see notes in Fut.wait_block about the required discipline to avoid deadlocks).

  • raises Shutdown

    if the runner was already shut down

val dummy : t

Runner that fails when scheduling tasks on it. Calling run_async on it will raise Failure.

  • since 0.6

Implementing runners

module For_runner_implementors : sig ... end

This module is specifically intended for users who implement their own runners. Regular users of Moonpool should not need to look at it.

val get_current_runner : unit -> t option

Access the current runner. This returns Some r if the call happens on a thread that belongs in a runner.

  • since 0.5
val get_current_storage : unit -> Task_local_storage.t option

get_current_storage runner gets the local storage for the currently running task.

+Runner (moonpool.Moonpool.Runner)

Module Moonpool.Runner

Interface for runners.

This provides an abstraction for running tasks in the background, which is implemented by various thread pools.

  • since 0.3
type fiber = Picos.Fiber.t
type task = unit -> unit
type t

A runner.

If a runner is no longer needed, shutdown can be used to signal all worker threads in it to stop (after they finish their work), and wait for them to stop.

The threads are distributed across a fixed domain pool (whose size is determined by Domain.recommended_domain_count on OCaml 5, and simple the single runtime on OCaml 4).

val size : t -> int

Number of threads/workers.

val num_tasks : t -> int

Current number of tasks. This is at best a snapshot, useful for metrics and debugging.

val shutdown : t -> unit

Shutdown the runner and wait for it to terminate. Idempotent.

val shutdown_without_waiting : t -> unit

Shutdown the pool, and do not wait for it to terminate. Idempotent.

exception Shutdown
val run_async : ?fiber:fiber -> t -> task -> unit

run_async pool f schedules f for later execution on the runner in one of the threads. f() will run on one of the runner's worker threads/domains.

  • parameter fiber

    if provided, run the task with this initial fiber data

  • raises Shutdown

    if the runner was shut down before run_async was called.

val run_wait_block : ?fiber:fiber -> t -> (unit -> 'a) -> 'a

run_wait_block pool f schedules f for later execution on the pool, like run_async. It then blocks the current thread until f() is done executing, and returns its result. If f() raises an exception, then run_wait_block pool f will raise it as well.

NOTE be careful with deadlocks (see notes in Fut.wait_block about the required discipline to avoid deadlocks).

  • raises Shutdown

    if the runner was already shut down

val dummy : t

Runner that fails when scheduling tasks on it. Calling run_async on it will raise Failure.

  • since 0.6

Implementing runners

module For_runner_implementors : sig ... end

This module is specifically intended for users who implement their own runners. Regular users of Moonpool should not need to look at it.

val get_current_runner : unit -> t option

Access the current runner. This returns Some r if the call happens on a thread that belongs in a runner.

  • since 0.5
val get_current_fiber : unit -> fiber option

get_current_storage runner gets the local storage for the currently running task.

diff --git a/moonpool/Moonpool/Task_local_storage/Direct/index.html b/moonpool/Moonpool/Task_local_storage/Direct/index.html deleted file mode 100644 index 8e3b0e05..00000000 --- a/moonpool/Moonpool/Task_local_storage/Direct/index.html +++ /dev/null @@ -1,2 +0,0 @@ - -Direct (moonpool.Moonpool.Task_local_storage.Direct)

Module Task_local_storage.Direct

Direct access to values from a storage handle

val get : t -> 'a key -> 'a

Access a key

val set : t -> 'a key -> 'a -> unit
val create : unit -> t
val copy : t -> t
diff --git a/moonpool/Moonpool/Task_local_storage/index.html b/moonpool/Moonpool/Task_local_storage/index.html index 01678803..2492e9c4 100644 --- a/moonpool/Moonpool/Task_local_storage/index.html +++ b/moonpool/Moonpool/Task_local_storage/index.html @@ -1,8 +1,2 @@ -Task_local_storage (moonpool.Moonpool.Task_local_storage)

Module Moonpool.Task_local_storage

Task-local storage.

This storage is associated to the current task, just like thread-local storage is associated with the current thread. The storage is carried along in case the current task is suspended.

  • since 0.6
type t = Moonpool__.Types_.local_storage

Underlying storage for a task. This is mutable and not thread-safe.

val dummy : t
type 'a key

A key used to access a particular (typed) storage slot on every task.

val new_key : init:(unit -> 'a) -> unit -> 'a key

new_key ~init () makes a new key. Keys are expensive and should never be allocated dynamically or in a loop. The correct pattern is, at toplevel:

  let k_foo : foo Task_ocal_storage.key =
-    Task_local_storage.new_key ~init:(fun () -> make_foo ()) ()
-
-(* … *)
-
-(* use it: *)
-let … = Task_local_storage.get k_foo
val get : 'a key -> 'a

get k gets the value for the current task for key k. Must be run from inside a task running on a runner.

  • raises Failure

    otherwise

val get_opt : 'a key -> 'a option

get_opt k gets the current task's value for key k, or None if not run from inside the task.

val set : 'a key -> 'a -> unit

set k v sets the storage for k to v. Must be run from inside a task running on a runner.

  • raises Failure

    otherwise

val with_value : 'a key -> 'a -> (unit -> 'b) -> 'b

with_value k v f sets k to v for the duration of the call to f(). When f() returns (or fails), k is restored to its old value.

val get_current : unit -> t option

Access the current storage, or None if not run from within a task.

module Direct : sig ... end

Direct access to values from a storage handle

+Task_local_storage (moonpool.Moonpool.Task_local_storage)

Module Moonpool.Task_local_storage

Task-local storage.

This storage is associated to the current task, just like thread-local storage is associated with the current thread. The storage is carried along in case the current task is suspended.

  • since 0.6
type 'a t = 'a Picos.Fiber.FLS.t
val create : unit -> 'a t

create () makes a new key. Keys are expensive and should never be allocated dynamically or in a loop.

exception Not_set
val get_exn : 'a t -> 'a

get k gets the value for the current task for key k. Must be run from inside a task running on a runner.

val get_opt : 'a t -> 'a option

get_opt k gets the current task's value for key k, or None if not run from inside the task.

val get : 'a t -> default:'a -> 'a
val set : 'a t -> 'a -> unit

set k v sets the storage for k to v. Must be run from inside a task running on a runner.

  • raises Failure

    otherwise

val with_value : 'a t -> 'a -> (unit -> 'b) -> 'b

with_value k v f sets k to v for the duration of the call to f(). When f() returns (or fails), k is restored to its old value.

Local Hmap.t

This requires hmap to be installed.

diff --git a/moonpool/Moonpool/Trigger/index.html b/moonpool/Moonpool/Trigger/index.html new file mode 100644 index 00000000..f3b8f86b --- /dev/null +++ b/moonpool/Moonpool/Trigger/index.html @@ -0,0 +1,2 @@ + +Trigger (moonpool.Moonpool.Trigger)

Module Moonpool.Trigger

Triggers from picos

  • since NEXT_RELEASE
include module type of struct include Picos.Trigger end

Interface for suspending

Represents a trigger. A trigger can be in one of three states: initial, awaiting, or signaled.

ℹ️ Once a trigger becomes signaled it no longer changes state.

🏎️ A trigger in the initial and signaled states is a tiny object that does not hold onto any other objects.

val create : unit -> t

create () allocates a new trigger in the initial state.

val is_signaled : t -> bool

is_signaled trigger determines whether the trigger is in the signaled state.

This can be useful, for example, when a trigger is being inserted to multiple locations and might be signaled concurrently while doing so. In such a case one can periodically check with is_signaled trigger whether it makes sense to continue.

ℹ️ Computation.try_attach already checks that the trigger being inserted has not been signaled so when attaching a trigger to multiple computations there is no need to separately check with is_signaled.

val await : t -> (exn * Stdlib.Printexc.raw_backtrace) option

await trigger waits for the trigger to be signaled.

The return value is None in case the trigger has been signaled and the fiber was resumed normally. Otherwise the return value is Some (exn, bt), which indicates that the fiber has been canceled and the caller should raise the exception. In either case the caller is responsible for cleaning up. Usually this means making sure that no references to the trigger remain to avoid space leaks.

⚠️ As a rule of thumb, if you inserted the trigger to some data structure or attached it to some computation, then you are responsible for removing and detaching the trigger after await.

ℹ️ A trigger in the signaled state only takes a small constant amount of memory. Make sure that it is not possible for a program to accumulate unbounded numbers of signaled triggers under any circumstance.

⚠️ Only the owner or creator of a trigger may call await. It is considered an error to make multiple calls to await.

ℹ️ The behavior is that, unless await can return immediately,

  • on OCaml 5, await will perform the Await effect, and
  • on OCaml 4, await will call the await operation of the current handler.
  • raises Invalid_argument

    if the trigger was in the awaiting state, which means that multiple concurrent calls of await are being made.

Interface for resuming

val signal : t -> unit

signal trigger puts the trigger into the signaled state and calls the resume action, if any, attached using on_signal.

The intention is that calling signal trigger guarantees that any fiber awaiting the trigger will be resumed. However, when and whether a fiber having called await will be resumed normally or as canceled is determined by the scheduler that handles the Await effect.

ℹ️ Note that under normal circumstances, signal should never raise an exception. If an exception is raised by signal, it means that the handler of Await has a bug or some catastrophic failure has occurred.

⚠️ Do not call signal from an effect handler in a scheduler.

Interface for schedulers

val is_initial : t -> bool

is_initial trigger determines whether the trigger is in the initial or in the signaled state.

ℹ️ Consider using is_signaled instead of is_initial as in some contexts a trigger might reasonably be either in the initial or the awaiting state depending on the order in which things are being done.

  • raises Invalid_argument

    if the trigger was in the awaiting state.

val on_signal : t -> 'x -> 'y -> (t -> 'x -> 'y -> unit) -> bool

on_signal trigger x y resume attempts to attach the resume action to the trigger and transition the trigger to the awaiting state.

The return value is true in case the action was attached successfully. Otherwise the return value is false, which means that the trigger was already in the signaled state.

⚠️ The action that you attach to a trigger must be safe to call from any context that might end up signaling the trigger directly or indirectly through propagation. Unless you know, then you should assume that the resume action might be called from a different domain running in parallel with neither effect nor exception handlers and that if the attached action doesn't return the system may deadlock or if the action doesn't return quickly it may cause performance issues.

⚠️ It is considered an error to make multiple calls to on_signal with a specific trigger.

  • raises Invalid_argument

    if the trigger was in the awaiting state, which means that either the owner or creator of the trigger made concurrent calls to await or the handler called on_signal more than once.

  • alert handler Only a scheduler should call this in the handler of the Await effect to attach the scheduler specific resume action to the trigger. Annotate your effect handling function with [@alert "-handler"].
val from_action : 'x -> 'y -> (t -> 'x -> 'y -> unit) -> t

from_action x y resume is equivalent to let t = create () in assert (on_signal t x y resume); t.

⚠️ The action that you attach to a trigger must be safe to call from any context that might end up signaling the trigger directly or indirectly through propagation. Unless you know, then you should assume that the resume action might be called from a different domain running in parallel with neither effect nor exception handlers and that if the attached action doesn't return the system may deadlock or if the action doesn't return quickly it may cause performance issues.

⚠️ The returned trigger will be in the awaiting state, which means that it is an error to call await, on_signal, or dispose on it.

  • alert handler This is an escape hatch for experts implementing schedulers or structured concurrency mechanisms. If you know what you are doing, use [@alert "-handler"].
val dispose : t -> unit

dispose trigger transition the trigger from the initial state to the signaled state.

🚦 The intended use case of dispose is for use from the handler of Await to ensure that the trigger has been put to the signaled state after await returns.

  • raises Invalid_argument

    if the trigger was in the awaiting state.

type Stdlib.Effect.t += private
  1. | Await : t -> (exn * Stdlib.Printexc.raw_backtrace) option Stdlib.Effect.t

Schedulers must handle the Await effect to implement the behavior of await.

In case the fiber permits propagation of cancelation, the trigger must be attached to the computation of the fiber for the duration of suspending the fiber by the scheduler.

Typically the scheduler calls try_suspend, which in turn calls on_signal, to attach a scheduler specific resume action to the trigger. The scheduler must guarantee that the fiber will be resumed after signal has been called on the trigger.

Whether being resumed due to cancelation or not, the trigger must be either signaled outside of the effect handler, or disposed by the effect handler, before resuming the fiber.

In case the fiber permits propagation of cancelation and the computation associated with the fiber has been canceled the scheduler is free to continue the fiber immediately with the cancelation exception after disposing the trigger.

⚠️ A scheduler must not discontinue, i.e. raise an exception to, the fiber as a response to Await.

The scheduler is free to choose which ready fiber to resume next.

Design rationale

A key idea behind this design is that the handler for Await does not need to run arbitrary user defined code while suspending a fiber: the handler calls on_signal by itself. This should make it easier to get both the handler and the user code correct.

Another key idea is that the signal operation provides no feedback as to the outcome regarding cancelation. Calling signal merely guarantees that the caller of await will return. This means that the point at which cancelation must be determined can be as late as possible. A scheduler can check the cancelation status just before calling continue and it is, of course, possible to check the cancelation status earlier. This allows maximal flexibility for the handler of Await.

The consequence of this is that the only place to handle cancelation is at the point of await. This makes the design simpler and should make it easier for the user to get the handling of cancelation right. A minor detail is that await returns an option instead of raising an exception. The reason for this is that matching against an option is slightly faster than setting up an exception handler. Returning an option also clearly communicates the two different cases to handle.

On the other hand, the trigger mechanism does not have a way to specify a user-defined callback to perform cancelation immediately before the fiber is resumed. Such an immediately called callback could be useful for e.g. canceling an underlying IO request. One justification for not having such a callback is that cancelation is allowed to take place from outside of the scheduler, i.e. from another system level thread, and, in such a case, the callback could not be called immediately. Instead, the scheduler is free to choose how to schedule canceled and continued fibers and, assuming that fibers can be trusted, a scheduler may give priority to canceled fibers.

This design also separates the allocation of the atomic state for the trigger, or create, from await, and allows the state to be polled using is_signaled before calling await. This is particularly useful when the trigger might need to be inserted to multiple places and be signaled in parallel before the call of await.

No mechanism is provided to communicate any result with the signal. That can be done outside of the mechanism and is often not needed. This simplifies the design.

Once signal has been called, a trigger no longer refers to any other object and takes just two words of memory. This e.g. allows lazy removal of triggers, assuming the number of attached triggers can be bounded, because nothing except the trigger itself would be leaked.

To further understand the problem domain, in this design, in a suspend-resume scenario, there are three distinct pieces of state:

  1. The state of shared data structure(s) used for communication and / or synchronization.
  2. The state of the trigger.
  3. The cancelation status of the fiber.

The trigger and cancelation status are both updated independently and atomically through code in this interface. The key requirement left for the user is to make sure that the state of the shared data structure is updated correctly independently of what await returns. So, for example, a mutex implementation must check, after getting Some (exn, bt), what the state of the mutex is and how it should be updated.

val await_exn : t -> unit
diff --git a/moonpool/Moonpool/Ws_pool/For_runner_implementors/index.html b/moonpool/Moonpool/Ws_pool/For_runner_implementors/index.html index eb9c1162..2fceb56e 100644 --- a/moonpool/Moonpool/Ws_pool/For_runner_implementors/index.html +++ b/moonpool/Moonpool/Ws_pool/For_runner_implementors/index.html @@ -3,6 +3,6 @@ size:(unit -> int) -> num_tasks:(unit -> int) -> shutdown:(wait:bool -> unit -> unit) -> - run_async:(ls:Task_local_storage.t -> task -> unit) -> + run_async:(fiber:fiber -> task -> unit) -> unit -> - t

Create a new runner.

NOTE: the runner should support DLA and Suspend_ on OCaml 5.x, so that Fork_join and other 5.x features work properly.

Key that should be used by each runner to store itself in TLS on every thread it controls, so that tasks running on these threads can access the runner. This is necessary for get_current_runner to work.

+ t

Create a new runner.

NOTE: the runner should support DLA and Suspend_ on OCaml 5.x, so that Fork_join and other 5.x features work properly.

val k_cur_runner : t Thread_local_storage.t

Key that should be used by each runner to store itself in TLS on every thread it controls, so that tasks running on these threads can access the runner. This is necessary for get_current_runner to work.

diff --git a/moonpool/Moonpool/Ws_pool/index.html b/moonpool/Moonpool/Ws_pool/index.html index c0235208..3920517e 100644 --- a/moonpool/Moonpool/Ws_pool/index.html +++ b/moonpool/Moonpool/Ws_pool/index.html @@ -1,5 +1,5 @@ -Ws_pool (moonpool.Moonpool.Ws_pool)

Module Moonpool.Ws_pool

Work-stealing thread pool.

A pool of threads with a worker-stealing scheduler. The pool contains a fixed number of threads that wait for work items to come, process these, and loop.

This is good for CPU-intensive tasks that feature a lot of small tasks. Note that tasks will not always be processed in the order they are scheduled, so this is not great for workloads where the latency of individual tasks matter (for that see Fifo_pool).

This implements Runner.t since 0.3.

If a pool is no longer needed, shutdown can be used to signal all threads in it to stop (after they finish their work), and wait for them to stop.

The threads are distributed across a fixed domain pool (whose size is determined by Domain.recommended_domain_count on OCaml 5, and simply the single runtime on OCaml 4).

include module type of Runner
type task = unit -> unit
type t

A runner.

If a runner is no longer needed, shutdown can be used to signal all worker threads in it to stop (after they finish their work), and wait for them to stop.

The threads are distributed across a fixed domain pool (whose size is determined by Domain.recommended_domain_count on OCaml 5, and simple the single runtime on OCaml 4).

val size : t -> int

Number of threads/workers.

val num_tasks : t -> int

Current number of tasks. This is at best a snapshot, useful for metrics and debugging.

val shutdown : t -> unit

Shutdown the runner and wait for it to terminate. Idempotent.

val shutdown_without_waiting : t -> unit

Shutdown the pool, and do not wait for it to terminate. Idempotent.

exception Shutdown
val run_async : ?ls:Task_local_storage.t -> t -> task -> unit

run_async pool f schedules f for later execution on the runner in one of the threads. f() will run on one of the runner's worker threads/domains.

  • parameter ls

    if provided, run the task with this initial local storage

  • raises Shutdown

    if the runner was shut down before run_async was called.

val run_wait_block : ?ls:Task_local_storage.t -> t -> (unit -> 'a) -> 'a

run_wait_block pool f schedules f for later execution on the pool, like run_async. It then blocks the current thread until f() is done executing, and returns its result. If f() raises an exception, then run_wait_block pool f will raise it as well.

NOTE be careful with deadlocks (see notes in Fut.wait_block about the required discipline to avoid deadlocks).

  • raises Shutdown

    if the runner was already shut down

val dummy : t

Runner that fails when scheduling tasks on it. Calling run_async on it will raise Failure.

  • since 0.6

Implementing runners

module For_runner_implementors : sig ... end

This module is specifically intended for users who implement their own runners. Regular users of Moonpool should not need to look at it.

val get_current_runner : unit -> t option

Access the current runner. This returns Some r if the call happens on a thread that belongs in a runner.

  • since 0.5
val get_current_storage : unit -> Task_local_storage.t option

get_current_storage runner gets the local storage for the currently running task.

type ('a, 'b) create_args = +Ws_pool (moonpool.Moonpool.Ws_pool)

Module Moonpool.Ws_pool

Work-stealing thread pool.

A pool of threads with a worker-stealing scheduler. The pool contains a fixed number of threads that wait for work items to come, process these, and loop.

This is good for CPU-intensive tasks that feature a lot of small tasks. Note that tasks will not always be processed in the order they are scheduled, so this is not great for workloads where the latency of individual tasks matter (for that see Fifo_pool).

This implements Runner.t since 0.3.

If a pool is no longer needed, shutdown can be used to signal all threads in it to stop (after they finish their work), and wait for them to stop.

The threads are distributed across a fixed domain pool (whose size is determined by Domain.recommended_domain_count on OCaml 5, and simply the single runtime on OCaml 4).

include module type of Runner
type fiber = Picos.Fiber.t
type task = unit -> unit
type t

A runner.

If a runner is no longer needed, shutdown can be used to signal all worker threads in it to stop (after they finish their work), and wait for them to stop.

The threads are distributed across a fixed domain pool (whose size is determined by Domain.recommended_domain_count on OCaml 5, and simple the single runtime on OCaml 4).

val size : t -> int

Number of threads/workers.

val num_tasks : t -> int

Current number of tasks. This is at best a snapshot, useful for metrics and debugging.

val shutdown : t -> unit

Shutdown the runner and wait for it to terminate. Idempotent.

val shutdown_without_waiting : t -> unit

Shutdown the pool, and do not wait for it to terminate. Idempotent.

exception Shutdown
val run_async : ?fiber:fiber -> t -> task -> unit

run_async pool f schedules f for later execution on the runner in one of the threads. f() will run on one of the runner's worker threads/domains.

  • parameter fiber

    if provided, run the task with this initial fiber data

  • raises Shutdown

    if the runner was shut down before run_async was called.

val run_wait_block : ?fiber:fiber -> t -> (unit -> 'a) -> 'a

run_wait_block pool f schedules f for later execution on the pool, like run_async. It then blocks the current thread until f() is done executing, and returns its result. If f() raises an exception, then run_wait_block pool f will raise it as well.

NOTE be careful with deadlocks (see notes in Fut.wait_block about the required discipline to avoid deadlocks).

  • raises Shutdown

    if the runner was already shut down

val dummy : t

Runner that fails when scheduling tasks on it. Calling run_async on it will raise Failure.

  • since 0.6

Implementing runners

module For_runner_implementors : sig ... end

This module is specifically intended for users who implement their own runners. Regular users of Moonpool should not need to look at it.

val get_current_runner : unit -> t option

Access the current runner. This returns Some r if the call happens on a thread that belongs in a runner.

  • since 0.5
val get_current_fiber : unit -> fiber option

get_current_storage runner gets the local storage for the currently running task.

type ('a, 'b) create_args = ?on_init_thread:(dom_id:int -> t_id:int -> unit -> unit) -> ?on_exit_thread:(dom_id:int -> t_id:int -> unit -> unit) -> ?on_exn:(exn -> Stdlib.Printexc.raw_backtrace -> unit) -> diff --git a/moonpool/Moonpool/index.html b/moonpool/Moonpool/index.html index a7ca3bb3..b6b1d501 100644 --- a/moonpool/Moonpool/index.html +++ b/moonpool/Moonpool/index.html @@ -1,2 +1,2 @@ -Moonpool (moonpool.Moonpool)

Module Moonpool

Moonpool

A pool within a bigger pool (ie the ocean). Here, we're talking about pools of Thread.t that are dispatched over several Domain.t to enable parallelism.

We provide several implementations of pools with distinct scheduling strategies, alongside some concurrency primitives such as guarding locks (Lock.t) and futures (Fut.t).

module Ws_pool : sig ... end

Work-stealing thread pool.

module Fifo_pool : sig ... end

A simple thread pool in FIFO order.

module Background_thread : sig ... end

A simple runner with a single background thread.

module Runner : sig ... end

Interface for runners.

module Immediate_runner : sig ... end

Runner that runs tasks in the caller thread.

module Exn_bt : sig ... end

Exception with backtrace.

exception Shutdown

Exception raised when trying to run tasks on runners that have been shut down.

  • since 0.6
val start_thread_on_some_domain : ('a -> unit) -> 'a -> Thread.t

Similar to Thread.create, but it picks a background domain at random to run the thread. This ensures that we don't always pick the same domain to run all the various threads needed in an application (timers, event loops, etc.)

val run_async : ?ls:Task_local_storage.t -> Runner.t -> (unit -> unit) -> unit

run_async runner task schedules the task to run on the given runner. This means task() will be executed at some point in the future, possibly in another thread.

  • since 0.5
val run_wait_block : ?ls:Task_local_storage.t -> Runner.t -> (unit -> 'a) -> 'a

run_wait_block runner f schedules f for later execution on the runner, like run_async. It then blocks the current thread until f() is done executing, and returns its result. If f() raises an exception, then run_wait_block pool f will raise it as well.

NOTE be careful with deadlocks (see notes in Fut.wait_block about the required discipline to avoid deadlocks).

  • raises Shutdown

    if the runner was already shut down

  • since 0.6

Number of threads recommended to saturate the CPU. For IO pools this makes little sense (you might want more threads than this because many of them will be blocked most of the time).

  • since 0.5
val spawn : on:Runner.t -> (unit -> 'a) -> 'a Fut.t

spawn ~on f runs f() on the runner (a thread pool typically) and returns a future result for it. See Fut.spawn.

  • since 0.5
val spawn_on_current_runner : (unit -> 'a) -> 'a Fut.t
val await : 'a Fut.t -> 'a

Await a future. See await. Only on OCaml >= 5.0.

  • since 0.5
module Lock : sig ... end

Mutex-protected resource.

module Fut : sig ... end

Futures.

module Chan : sig ... end

Channels.

module Task_local_storage : sig ... end

Task-local storage.

module Thread_local_storage = Moonpool_private.Thread_local_storage_
module Blocking_queue : sig ... end

A simple blocking queue.

module Bounded_queue : sig ... end

A blocking queue of finite size.

module Atomic = Moonpool_private.Atomic_

Atomic values.

+Moonpool (moonpool.Moonpool)

Module Moonpool

Moonpool

A pool within a bigger pool (ie the ocean). Here, we're talking about pools of Thread.t that are dispatched over several Domain.t to enable parallelism.

We provide several implementations of pools with distinct scheduling strategies, alongside some concurrency primitives such as guarding locks (Lock.t) and futures (Fut.t).

module Ws_pool : sig ... end

Work-stealing thread pool.

module Fifo_pool : sig ... end

A simple thread pool in FIFO order.

module Background_thread : sig ... end

A simple runner with a single background thread.

module Runner : sig ... end

Interface for runners.

module Trigger : sig ... end

Triggers from picos

module Immediate_runner : sig ... end

Runner that runs tasks in the caller thread.

module Exn_bt : sig ... end

Exception with backtrace.

exception Shutdown

Exception raised when trying to run tasks on runners that have been shut down.

  • since 0.6
val start_thread_on_some_domain : ('a -> unit) -> 'a -> Thread.t

Similar to Thread.create, but it picks a background domain at random to run the thread. This ensures that we don't always pick the same domain to run all the various threads needed in an application (timers, event loops, etc.)

val run_async : ?fiber:Picos.Fiber.t -> Runner.t -> (unit -> unit) -> unit

run_async runner task schedules the task to run on the given runner. This means task() will be executed at some point in the future, possibly in another thread.

  • parameter fiber

    optional initial (picos) fiber state

  • since 0.5
val run_wait_block : ?fiber:Picos.Fiber.t -> Runner.t -> (unit -> 'a) -> 'a

run_wait_block runner f schedules f for later execution on the runner, like run_async. It then blocks the current thread until f() is done executing, and returns its result. If f() raises an exception, then run_wait_block pool f will raise it as well.

See run_async for more details.

NOTE be careful with deadlocks (see notes in Fut.wait_block about the required discipline to avoid deadlocks).

  • raises Shutdown

    if the runner was already shut down

  • since 0.6

Number of threads recommended to saturate the CPU. For IO pools this makes little sense (you might want more threads than this because many of them will be blocked most of the time).

  • since 0.5
val spawn : on:Runner.t -> (unit -> 'a) -> 'a Fut.t

spawn ~on f runs f() on the runner (a thread pool typically) and returns a future result for it. See Fut.spawn.

  • since 0.5
val spawn_on_current_runner : (unit -> 'a) -> 'a Fut.t
val await : 'a Fut.t -> 'a

Await a future. See await. Only on OCaml >= 5.0.

  • since 0.5
module Lock : sig ... end

Mutex-protected resource.

module Fut : sig ... end

Futures.

module Chan : sig ... end

Channels.

module Task_local_storage : sig ... end

Task-local storage.

module Thread_local_storage = Thread_local_storage
module Blocking_queue : sig ... end

A simple blocking queue.

module Bounded_queue : sig ... end

A blocking queue of finite size.

module Atomic = Moonpool_private.Atomic_

Atomic values.

diff --git a/moonpool/Moonpool__Suspend_/index.html b/moonpool/Moonpool__Hmap_ls_/index.html similarity index 74% rename from moonpool/Moonpool__Suspend_/index.html rename to moonpool/Moonpool__Hmap_ls_/index.html index 1526a16b..3849c01f 100644 --- a/moonpool/Moonpool__Suspend_/index.html +++ b/moonpool/Moonpool__Hmap_ls_/index.html @@ -1,2 +1,2 @@ -Moonpool__Suspend_ (moonpool.Moonpool__Suspend_)

Module Moonpool__Suspend_

This module is hidden.

+Moonpool__Hmap_ls_ (moonpool.Moonpool__Hmap_ls_)

Module Moonpool__Hmap_ls_

This module is hidden.

diff --git a/moonpool/Moonpool__Trigger/index.html b/moonpool/Moonpool__Trigger/index.html new file mode 100644 index 00000000..810cd19e --- /dev/null +++ b/moonpool/Moonpool__Trigger/index.html @@ -0,0 +1,2 @@ + +Moonpool__Trigger (moonpool.Moonpool__Trigger)

Module Moonpool__Trigger

This module is hidden.

diff --git a/moonpool/Moonpool__Types_/index.html b/moonpool/Moonpool__Types_/index.html new file mode 100644 index 00000000..208a4e79 --- /dev/null +++ b/moonpool/Moonpool__Types_/index.html @@ -0,0 +1,2 @@ + +Moonpool__Types_ (moonpool.Moonpool__Types_)

Module Moonpool__Types_

This module is hidden.

diff --git a/moonpool/Moonpool_private__Dla_/index.html b/moonpool/Moonpool__Worker_loop_/index.html similarity index 66% rename from moonpool/Moonpool_private__Dla_/index.html rename to moonpool/Moonpool__Worker_loop_/index.html index e22af877..a4f17533 100644 --- a/moonpool/Moonpool_private__Dla_/index.html +++ b/moonpool/Moonpool__Worker_loop_/index.html @@ -1,2 +1,2 @@ -Moonpool_private__Dla_ (moonpool.Moonpool_private__Dla_)

Module Moonpool_private__Dla_

This module is hidden.

+Moonpool__Worker_loop_ (moonpool.Moonpool__Worker_loop_)

Module Moonpool__Worker_loop_

This module is hidden.

diff --git a/moonpool/Moonpool_fib/Fls/index.html b/moonpool/Moonpool_fib/Fls/index.html index 98ad6865..04d5d9fd 100644 --- a/moonpool/Moonpool_fib/Fls/index.html +++ b/moonpool/Moonpool_fib/Fls/index.html @@ -1,8 +1,2 @@ -Fls (moonpool.Moonpool_fib.Fls)

Module Moonpool_fib.Fls

Fiber-local storage.

This storage is associated to the current fiber, just like thread-local storage is associated with the current thread.

See Moonpool.Task_local_storage for more general information, as this is based on it.

NOTE: it's important to note that, while each fiber has its own storage, spawning a sub-fiber f2 from a fiber f1 will only do a shallow copy of the storage. Values inside f1's storage will be physically shared with f2. It is thus recommended to store only persistent values in the local storage.

include module type of struct include Moonpool.Task_local_storage end
type t = Moonpool__.Types_.local_storage

Underlying storage for a task. This is mutable and not thread-safe.

val dummy : t

A key used to access a particular (typed) storage slot on every task.

val new_key : init:(unit -> 'a) -> unit -> 'a key

new_key ~init () makes a new key. Keys are expensive and should never be allocated dynamically or in a loop. The correct pattern is, at toplevel:

  let k_foo : foo Task_ocal_storage.key =
-    Task_local_storage.new_key ~init:(fun () -> make_foo ()) ()
-
-(* … *)
-
-(* use it: *)
-let … = Task_local_storage.get k_foo
val get : 'a key -> 'a

get k gets the value for the current task for key k. Must be run from inside a task running on a runner.

  • raises Failure

    otherwise

val get_opt : 'a key -> 'a option

get_opt k gets the current task's value for key k, or None if not run from inside the task.

val set : 'a key -> 'a -> unit

set k v sets the storage for k to v. Must be run from inside a task running on a runner.

  • raises Failure

    otherwise

val with_value : 'a key -> 'a -> (unit -> 'b) -> 'b

with_value k v f sets k to v for the duration of the call to f(). When f() returns (or fails), k is restored to its old value.

val get_current : unit -> t option

Access the current storage, or None if not run from within a task.

Direct access to values from a storage handle

+Fls (moonpool.Moonpool_fib.Fls)

Module Moonpool_fib.Fls

Fiber-local storage.

This storage is associated to the current fiber, just like thread-local storage is associated with the current thread.

See Moonpool.Task_local_storage for more general information, as this is based on it.

NOTE: it's important to note that, while each fiber has its own storage, spawning a sub-fiber f2 from a fiber f1 will only do a shallow copy of the storage. Values inside f1's storage will be physically shared with f2. It is thus recommended to store only persistent values in the local storage.

include module type of struct include Moonpool.Task_local_storage end
type 'a t = 'a Picos.Fiber.FLS.t
val create : unit -> 'a t

create () makes a new key. Keys are expensive and should never be allocated dynamically or in a loop.

exception Not_set
val get_exn : 'a t -> 'a

get k gets the value for the current task for key k. Must be run from inside a task running on a runner.

val get_opt : 'a t -> 'a option

get_opt k gets the current task's value for key k, or None if not run from inside the task.

val get : 'a t -> default:'a -> 'a
val set : 'a t -> 'a -> unit

set k v sets the storage for k to v. Must be run from inside a task running on a runner.

  • raises Failure

    otherwise

val with_value : 'a t -> 'a -> (unit -> 'b) -> 'b

with_value k v f sets k to v for the duration of the call to f(). When f() returns (or fails), k is restored to its old value.

Local Hmap.t

This requires hmap to be installed.

diff --git a/moonpool/Moonpool_private/Dla_/index.html b/moonpool/Moonpool_private/Dla_/index.html deleted file mode 100644 index b0db8943..00000000 --- a/moonpool/Moonpool_private/Dla_/index.html +++ /dev/null @@ -1,2 +0,0 @@ - -Dla_ (moonpool.Moonpool_private.Dla_)

Module Moonpool_private.Dla_

Interface to Domain-local-await.

This is used to handle the presence or absence of DLA.

type t = {
  1. release : unit -> unit;
  2. await : unit -> unit;
}
val using : prepare_for_await:(unit -> t) -> while_running:(unit -> 'a) -> 'a
val setup_domain : unit -> unit
diff --git a/moonpool/Moonpool_private/Thread_local_storage_/index.html b/moonpool/Moonpool_private/Thread_local_storage_/index.html deleted file mode 100644 index 6c98a3fe..00000000 --- a/moonpool/Moonpool_private/Thread_local_storage_/index.html +++ /dev/null @@ -1,2 +0,0 @@ - -Thread_local_storage_ (moonpool.Moonpool_private.Thread_local_storage_)

Module Moonpool_private.Thread_local_storage_

Thread local storage

type 'a t

A TLS slot for values of type 'a. This allows the storage of a single value of type 'a per thread.

exception Not_set
val create : unit -> 'a t
val get_exn : 'a t -> 'a
val get_opt : 'a t -> 'a option
val set : 'a t -> 'a -> unit
diff --git a/moonpool/Moonpool_private/Ws_deque_/index.html b/moonpool/Moonpool_private/Ws_deque_/index.html index f354be7f..c5a64d3e 100644 --- a/moonpool/Moonpool_private/Ws_deque_/index.html +++ b/moonpool/Moonpool_private/Ws_deque_/index.html @@ -1,2 +1,2 @@ -Ws_deque_ (moonpool.Moonpool_private.Ws_deque_)

Module Moonpool_private.Ws_deque_

Work-stealing deque.

Adapted from "Dynamic circular work stealing deque", Chase & Lev.

However note that this one is not dynamic in the sense that there is no resizing. Instead we return false when push fails, which keeps the implementation fairly lightweight.

type 'a t

Deque containing values of type 'a

val create : dummy:'a -> unit -> 'a t

Create a new deque.

val push : 'a t -> 'a -> bool

Push value at the bottom of deque. returns true if it succeeds. This must be called only by the owner thread.

val pop : 'a t -> 'a option

Pop value from the bottom of deque. This must be called only by the owner thread.

val steal : 'a t -> 'a option

Try to steal from the top of deque. This is thread-safe.

val size : _ t -> int
+Ws_deque_ (moonpool.Moonpool_private.Ws_deque_)

Module Moonpool_private.Ws_deque_

Work-stealing deque.

Adapted from "Dynamic circular work stealing deque", Chase & Lev.

However note that this one is not dynamic in the sense that there is no resizing. Instead we return false when push fails, which keeps the implementation fairly lightweight.

type 'a t

Deque containing values of type 'a

val create : dummy:'a -> unit -> 'a t

Create a new deque.

val push : 'a t -> 'a -> bool

Push value at the bottom of deque. returns true if it succeeds. This must be called only by the owner thread.

val pop : 'a t -> 'a option

Pop value from the bottom of deque. This must be called only by the owner thread.

exception Empty
val pop_exn : 'a t -> 'a
val steal : 'a t -> 'a option

Try to steal from the top of deque. This is thread-safe.

val size : _ t -> int
diff --git a/moonpool/Moonpool_private/index.html b/moonpool/Moonpool_private/index.html index d121dd1d..44d533cf 100644 --- a/moonpool/Moonpool_private/index.html +++ b/moonpool/Moonpool_private/index.html @@ -1,2 +1,2 @@ -Moonpool_private (moonpool.Moonpool_private)

Module Moonpool_private

module Atomic_ : sig ... end
module Dla_ : sig ... end

Interface to Domain-local-await.

module Domain_ : sig ... end
module Thread_local_storage_ : sig ... end

Thread local storage

module Tracing_ : sig ... end
module Ws_deque_ : sig ... end

Work-stealing deque.

+Moonpool_private (moonpool.Moonpool_private)

Module Moonpool_private

module Atomic_ : sig ... end
module Domain_ : sig ... end
module Tracing_ : sig ... end
module Ws_deque_ : sig ... end

Work-stealing deque.

diff --git a/moonpool/Moonpool_private__Thread_local_storage_/index.html b/moonpool/Moonpool_private__Thread_local_storage_/index.html deleted file mode 100644 index ba75e34e..00000000 --- a/moonpool/Moonpool_private__Thread_local_storage_/index.html +++ /dev/null @@ -1,2 +0,0 @@ - -Moonpool_private__Thread_local_storage_ (moonpool.Moonpool_private__Thread_local_storage_)

Module Moonpool_private__Thread_local_storage_

This module is hidden.

diff --git a/moonpool/Moonpool_sync/Event/Infix/index.html b/moonpool/Moonpool_sync/Event/Infix/index.html new file mode 100644 index 00000000..f1accfd1 --- /dev/null +++ b/moonpool/Moonpool_sync/Event/Infix/index.html @@ -0,0 +1,2 @@ + +Infix (moonpool.Moonpool_sync.Event.Infix)

Module Event.Infix

val (>|=) : 'a t -> ('a -> 'b) -> 'b t
val let+ : 'a t -> ('a -> 'b) -> 'b t
diff --git a/moonpool/Moonpool_sync/Event/index.html b/moonpool/Moonpool_sync/Event/index.html new file mode 100644 index 00000000..d4d262ff --- /dev/null +++ b/moonpool/Moonpool_sync/Event/index.html @@ -0,0 +1,2 @@ + +Event (moonpool.Moonpool_sync.Event)

Module Moonpool_sync.Event

include module type of struct include Picos_std_event.Event end
type !'a t = 'a Picos_std_event.Event.t

An event returning a value of type 'a.

type 'a event = 'a t

An alias for the Event.t type to match the Event module signature.

val always : 'a -> 'a t

always value returns an event that can always be committed to resulting in the given value.

Composing events

val choose : 'a t list -> 'a t

choose events return an event that offers all of the given events and then commits to at most one of them.

val wrap : 'b t -> ('b -> 'a) -> 'a t

wrap event fn returns an event that acts as the given event and then applies the given function to the value in case the event is committed to.

val map : ('b -> 'a) -> 'b t -> 'a t

map fn event is equivalent to wrap event fn.

val guard : (unit -> 'a t) -> 'a t

guard thunk returns an event that, when synchronized, calls the thunk, and then behaves like the resulting event.

⚠️ Raising an exception from a guard thunk will result in raising that exception out of the sync. This may result in dropping the result of an event that committed just after the exception was raised. This means that you should treat an unexpected exception raised from sync as a fatal error.

Consuming events

val sync : 'a t -> 'a

sync event synchronizes on the given event.

Synchronizing on an event executes in three phases:

  1. In the first phase offers or requests are made to communicate.
  2. One of the offers or requests is committed to and all the other offers and requests are canceled.
  3. A final result is computed from the value produced by the event.

⚠️ sync event does not wait for the canceled concurrent requests to terminate. This means that you should arrange for guaranteed cleanup through other means such as the use of structured concurrency.

val select : 'a t list -> 'a

select events is equivalent to sync (choose events).

Primitive events

ℹ️ The Computation concept of Picos can be seen as a basic single-shot atomic event. This module builds on that concept to provide a composable API to concurrent services exposed through computations.

type 'a request = 'a Picos_std_event.Event.request = {
  1. request : 'r. (unit -> 'r) Picos.Computation.t -> ('a -> 'r) -> unit;
}

Represents a function that requests a concurrent service to update a computation.

ℹ️ The computation passed to a request may be completed by some other event at any point. All primitive requests should be implemented carefully to take that into account. If the computation is completed by some other event, then the request should be considered as canceled, take no effect, and not leak any resources.

⚠️ Raising an exception from a request function will result in raising that exception out of the sync. This may result in dropping the result of an event that committed just after the exception was raised. This means that you should treat an unexpected exception raised from sync as a fatal error. In addition, you should arrange for concurrent services to report unexpected errors independently of the computation being passed to the service.

val from_request : 'a request -> 'a t

from_request { request } creates an event from the request function.

val from_computation : 'a Picos.Computation.t -> 'a t

from_computation source creates an event that can be committed to once the given source computation has completed.

ℹ️ Committing to some other event does not cancel the source computation.

val of_fut : 'a Moonpool.Fut.t -> 'a t
module Infix : sig ... end
include module type of Infix
val (>|=) : 'a t -> ('a -> 'b) -> 'b t
val let+ : 'a t -> ('a -> 'b) -> 'b t
diff --git a/moonpool/Moonpool_sync/Lock/index.html b/moonpool/Moonpool_sync/Lock/index.html new file mode 100644 index 00000000..1aa91e57 --- /dev/null +++ b/moonpool/Moonpool_sync/Lock/index.html @@ -0,0 +1,13 @@ + +Lock (moonpool.Moonpool_sync.Lock)

Module Moonpool_sync.Lock

Mutex-protected resource.

This lock is a synchronous concurrency primitive, as a thin wrapper around Mutex that encourages proper management of the critical section in RAII style:

let (let@) = (@@)
+
+
+…
+let compute_foo =
+  (* enter critical section *)
+  let@ x = Lock.with_ protected_resource in
+  use_x;
+  return_foo ()
+  (* exit critical section *)
+in
+…

This lock is based on Picos_sync.Mutex so it is await-safe.

  • since NEXT_RELEASE
type 'a t

A value protected by a cooperative mutex

val create : 'a -> 'a t

Create a new protected value.

val with_ : 'a t -> ('a -> 'b) -> 'b

with_ l f runs f x where x is the value protected with the lock l, in a critical section. If f x fails, with_lock l f fails too but the lock is released.

val update : 'a t -> ('a -> 'a) -> unit

update l f replaces the content x of l with f x, while protected by the mutex.

val update_map : 'a t -> ('a -> 'a * 'b) -> 'b

update_map l f computes x', y = f (get l), then puts x' in l and returns y, while protected by the mutex.

val mutex : _ t -> Picos_std_sync.Mutex.t

Underlying mutex.

val get : 'a t -> 'a

Atomically get the value in the lock. The value that is returned isn't protected!

val set : 'a t -> 'a -> unit

Atomically set the value.

NOTE caution: using get and set as if this were a ref is an anti pattern and will not protect data against some race conditions.

diff --git a/moonpool/Moonpool_sync/index.html b/moonpool/Moonpool_sync/index.html new file mode 100644 index 00000000..878b9019 --- /dev/null +++ b/moonpool/Moonpool_sync/index.html @@ -0,0 +1,2 @@ + +Moonpool_sync (moonpool.Moonpool_sync)

Module Moonpool_sync

module Mutex = Picos_std_sync.Mutex
module Condition = Picos_std_sync.Condition
module Lock : sig ... end

Mutex-protected resource.

module Event : sig ... end
module Semaphore = Picos_std_sync.Semaphore
module Lazy = Picos_std_sync.Lazy
module Latch = Picos_std_sync.Latch
module Ivar = Picos_std_sync.Ivar
module Stream = Picos_std_sync.Stream
diff --git a/moonpool/Moonpool_sync__/index.html b/moonpool/Moonpool_sync__/index.html new file mode 100644 index 00000000..519fa1f8 --- /dev/null +++ b/moonpool/Moonpool_sync__/index.html @@ -0,0 +1,2 @@ + +Moonpool_sync__ (moonpool.Moonpool_sync__)

Module Moonpool_sync__

This module is hidden.

diff --git a/moonpool/Moonpool_sync__Event/index.html b/moonpool/Moonpool_sync__Event/index.html new file mode 100644 index 00000000..46eb4257 --- /dev/null +++ b/moonpool/Moonpool_sync__Event/index.html @@ -0,0 +1,2 @@ + +Moonpool_sync__Event (moonpool.Moonpool_sync__Event)

Module Moonpool_sync__Event

This module is hidden.

diff --git a/moonpool/Moonpool_sync__Lock/index.html b/moonpool/Moonpool_sync__Lock/index.html new file mode 100644 index 00000000..e57b7435 --- /dev/null +++ b/moonpool/Moonpool_sync__Lock/index.html @@ -0,0 +1,2 @@ + +Moonpool_sync__Lock (moonpool.Moonpool_sync__Lock)

Module Moonpool_sync__Lock

This module is hidden.

diff --git a/moonpool/index.html b/moonpool/index.html index 47fca43b..962cf9ee 100644 --- a/moonpool/index.html +++ b/moonpool/index.html @@ -1,2 +1,2 @@ -index (moonpool.index)

Package moonpool

Package info

changes-files
readme-files
+index (moonpool.index)

Package moonpool

Package info

changes-files
readme-files
diff --git a/multicore-magic/Multicore_magic/Atomic_array/index.html b/multicore-magic/Multicore_magic/Atomic_array/index.html new file mode 100644 index 00000000..5e0ac21e --- /dev/null +++ b/multicore-magic/Multicore_magic/Atomic_array/index.html @@ -0,0 +1,2 @@ + +Atomic_array (multicore-magic.Multicore_magic.Atomic_array)

Module Multicore_magic.Atomic_array

Array of (potentially unboxed) atomic locations.

Where available, this uses an undocumented operation exported by the OCaml 5 runtime, caml_atomic_cas_field, which makes it possible to perform sequentially consistent atomic updates of record fields and array elements.

Hopefully a future version of OCaml provides more comprehensive and even more efficient support for both sequentially consistent and relaxed atomic operations on records and arrays.

type !'a t

Represents an array of atomic locations.

val make : int -> 'a -> 'a t

make n value creates a new array of n atomic locations having given value.

val of_array : 'a array -> 'a t

of_array non_atomic_array create a new array of atomic locations as a copy of the given non_atomic_array.

val init : int -> (int -> 'a) -> 'a t

init n fn is equivalent to of_array (Array.init n fn).

val length : 'a t -> int

length atomic_array returns the length of the atomic_array.

val unsafe_fenceless_get : 'a t -> int -> 'a

unsafe_fenceless_get atomic_array index reads and returns the value at the specified index of the atomic_array.

⚠️ The read is relaxed and may be reordered with respect to other reads and writes in program order.

⚠️ No bounds checking is performed.

val unsafe_fenceless_set : 'a t -> int -> 'a -> unit

unsafe_fenceless_set atomic_array index value writes the given value to the specified index of the atomic_array.

⚠️ The write is relaxed and may be reordered with respect to other reads and (non-initializing) writes in program order.

⚠️ No bounds checking is performed.

val unsafe_compare_and_set : 'a t -> int -> 'a -> 'a -> bool

unsafe_compare_and_set atomic_array index before after atomically updates the specified index of the atomic_array to the after value in case it had the before value and returns a boolean indicating whether that was the case. This operation is sequentially consistent and may not be reordered with respect to other reads and writes in program order.

⚠️ No bounds checking is performed.

diff --git a/multicore-magic/Multicore_magic/Transparent_atomic/index.html b/multicore-magic/Multicore_magic/Transparent_atomic/index.html new file mode 100644 index 00000000..707f4ff7 --- /dev/null +++ b/multicore-magic/Multicore_magic/Transparent_atomic/index.html @@ -0,0 +1,2 @@ + +Transparent_atomic (multicore-magic.Multicore_magic.Transparent_atomic)

Module Multicore_magic.Transparent_atomic

A replacement for Stdlib.Atomic with fixes and performance improvements

Stdlib.Atomic.get is incorrectly subject to CSE optimization in OCaml 5.0.0 and 5.1.0. This can result in code being generated that can produce results that cannot be explained with the OCaml memory model. It can also sometimes result in code being generated where a manual optimization to avoid writing to memory is defeated by the compiler as the compiler eliminates a (repeated) read access. This module implements get such that argument to Stdlib.Atomic.get is passed through Sys.opaque_identity, which prevents the compiler from applying the CSE optimization.

OCaml 5 generates inefficient accesses of 'a Stdlib.Atomic.t arrays assuming that the array might be an array of floating point numbers. That is because the Stdlib.Atomic.t type constructor is opaque, which means that the compiler cannot assume that _ Stdlib.Atomic.t is not the same as float. This module defines the type as private 'a ref, which allows the compiler to know that it cannot be the same as float, which allows the compiler to generate more efficient array accesses. This can both improve performance and reduce size of generated code when using arrays of atomics.

type !'a t = private 'a ref
val make : 'a -> 'a t
val make_contended : 'a -> 'a t
val get : 'a t -> 'a
val fenceless_get : 'a t -> 'a
val set : 'a t -> 'a -> unit
val fenceless_set : 'a t -> 'a -> unit
val exchange : 'a t -> 'a -> 'a
val compare_and_set : 'a t -> 'a -> 'a -> bool
val fetch_and_add : int t -> int -> int
val incr : int t -> unit
val decr : int t -> unit
diff --git a/multicore-magic/Multicore_magic/index.html b/multicore-magic/Multicore_magic/index.html new file mode 100644 index 00000000..baef4b35 --- /dev/null +++ b/multicore-magic/Multicore_magic/index.html @@ -0,0 +1,36 @@ + +Multicore_magic (multicore-magic.Multicore_magic)

Module Multicore_magic

This is a library of magic multicore utilities intended for experts for extracting the best possible performance from multicore OCaml.

Hopefully future releases of multicore OCaml will make this library obsolete!

Helpers for using padding to avoid false sharing

val copy_as_padded : 'a -> 'a

Depending on the object, either creates a shallow clone of it or returns it as is. When cloned, the clone will have extra padding words added after the last used word.

This is designed to help avoid false sharing. False sharing has a negative impact on multicore performance. Accesses of both atomic and non-atomic locations, whether read-only or read-write, may suffer from false sharing.

The intended use case for this is to pad all long lived objects that are being accessed highly frequently (read or written).

Many kinds of objects can be padded, for example:

let padded_atomic = Multicore_magic.copy_as_padded (Atomic.make 101)
+
+let padded_ref = Multicore_magic.copy_as_padded (ref 42)
+
+let padded_record = Multicore_magic.copy_as_padded {
+  number = 76;
+  pointer = 1 :: 2 :: 3 :: [];
+}
+
+let padded_variant = Multicore_magic.copy_as_padded (Some 1)

Padding changes the length of an array. If you need to pad an array, use make_padded_array.

val copy_as : ?padded:bool -> 'a -> 'a

copy_as x by default simply returns x. When ~padded:true is explicitly specified, returns copy_as_padded x.

val make_padded_array : int -> 'a -> 'a array

Creates a padded array. The length of the returned array includes padding. Use length_of_padded_array to get the unpadded length.

val length_of_padded_array : 'a array -> int

Returns the length of an array created by make_padded_array without the padding.

WARNING: This is not guaranteed to work with copy_as_padded.

val length_of_padded_array_minus_1 : 'a array -> int

Returns the length of an array created by make_padded_array without the padding minus 1.

WARNING: This is not guaranteed to work with copy_as_padded.

Missing Atomic operations

val fenceless_get : 'a Stdlib.Atomic.t -> 'a

Get a value from the atomic without performing an acquire fence.

Consider the following prototypical example of a lock-free algorithm:

let rec prototypical_lock_free_algorithm () =
+  let expected = Atomic.get atomic in
+  let desired = (* computed from expected *) in
+  if not (Atomic.compare_and_set atomic expected desired) then
+    (* failure, maybe retry *)
+  else
+    (* success *)

A potential performance problem with the above example is that it performs two acquire fences. Both the Atomic.get and the Atomic.compare_and_set perform an acquire fence. This may have a negative impact on performance.

Assuming the first fence is not necessary, we can rewrite the example using fenceless_get as follows:

let rec prototypical_lock_free_algorithm () =
+  let expected = Multicore_magic.fenceless_get atomic in
+  let desired = (* computed from expected *) in
+  if not (Atomic.compare_and_set atomic expected desired) then
+    (* failure, maybe retry *)
+  else
+    (* success *)

Now only a single acquire fence is performed by Atomic.compare_and_set and performance may be improved.

val fenceless_set : 'a Stdlib.Atomic.t -> 'a -> unit

Set the value of an atomic without performing a full fence.

Consider the following example:

let new_atomic = Atomic.make dummy_value in
+(* prepare data_structure referring to new_atomic *)
+Atomic.set new_atomic data_structure;
+(* publish the data_structure: *)
+Atomic.exchance old_atomic data_structure

A potential performance problem with the above example is that it performs two full fences. Both the Atomic.set used to initialize the data structure and the Atomic.exchange used to publish the data structure perform a full fence. The same would also apply in cases where Atomic.compare_and_set or Atomic.set would be used to publish the data structure. This may have a negative impact on performance.

Using fenceless_set we can rewrite the example as follows:

let new_atomic = Atomic.make dummy_value in
+(* prepare data_structure referring to new_atomic *)
+Multicore_magic.fenceless_set new_atomic data_structure;
+(* publish the data_structure: *)
+Atomic.exchance old_atomic data_structure

Now only a single full fence is performed by Atomic.exchange and performance may be improved.

val fence : int Stdlib.Atomic.t -> unit

Perform a full acquire-release fence on the given atomic.

fence atomic is equivalent to ignore (Atomic.fetch_and_add atomic 0).

Fixes and workarounds

module Transparent_atomic : sig ... end

A replacement for Stdlib.Atomic with fixes and performance improvements

Missing functionality

module Atomic_array : sig ... end

Array of (potentially unboxed) atomic locations.

Avoiding contention

val instantaneous_domain_index : unit -> int

instantaneous_domain_index () potentially (re)allocates and returns a non-negative integer "index" for the current domain. The indices are guaranteed to be unique among the domains that exist at a point in time. Each call of instantaneous_domain_index () may return a different index.

The intention is that the returned value can be used as an index into a contention avoiding parallelism safe data structure. For example, a naïve scalable increment of one counter from an array of counters could be done as follows:

let incr counters =
+  (* Assuming length of [counters] is a power of two and larger than
+     the number of domains. *)
+  let mask = Array.length counters - 1 in
+  let index = instantaneous_domain_index () in
+  Atomic.incr counters.(index land mask)

The implementation ensures that the indices are allocated as densely as possible at any given moment. This should allow allocating as many counters as needed and essentially eliminate contention.

On OCaml 4 instantaneous_domain_index () will always return 0.

diff --git a/multicore-magic/Multicore_magic__/index.html b/multicore-magic/Multicore_magic__/index.html new file mode 100644 index 00000000..06dd3d4a --- /dev/null +++ b/multicore-magic/Multicore_magic__/index.html @@ -0,0 +1,2 @@ + +Multicore_magic__ (multicore-magic.Multicore_magic__)

Module Multicore_magic__

This module is hidden.

diff --git a/multicore-magic/Multicore_magic__Cache/index.html b/multicore-magic/Multicore_magic__Cache/index.html new file mode 100644 index 00000000..3a839806 --- /dev/null +++ b/multicore-magic/Multicore_magic__Cache/index.html @@ -0,0 +1,2 @@ + +Multicore_magic__Cache (multicore-magic.Multicore_magic__Cache)

Module Multicore_magic__Cache

This module is hidden.

diff --git a/multicore-magic/Multicore_magic__Index/index.html b/multicore-magic/Multicore_magic__Index/index.html new file mode 100644 index 00000000..22d4b145 --- /dev/null +++ b/multicore-magic/Multicore_magic__Index/index.html @@ -0,0 +1,2 @@ + +Multicore_magic__Index (multicore-magic.Multicore_magic__Index)

Module Multicore_magic__Index

This module is hidden.

diff --git a/multicore-magic/Multicore_magic__Padding/index.html b/multicore-magic/Multicore_magic__Padding/index.html new file mode 100644 index 00000000..68560dfa --- /dev/null +++ b/multicore-magic/Multicore_magic__Padding/index.html @@ -0,0 +1,2 @@ + +Multicore_magic__Padding (multicore-magic.Multicore_magic__Padding)

Module Multicore_magic__Padding

This module is hidden.

diff --git a/multicore-magic/Multicore_magic__Transparent_atomic/index.html b/multicore-magic/Multicore_magic__Transparent_atomic/index.html new file mode 100644 index 00000000..ba37a05d --- /dev/null +++ b/multicore-magic/Multicore_magic__Transparent_atomic/index.html @@ -0,0 +1,2 @@ + +Multicore_magic__Transparent_atomic (multicore-magic.Multicore_magic__Transparent_atomic)

Module Multicore_magic__Transparent_atomic

This module is hidden.

diff --git a/multicore-magic/_doc-dir/CHANGES.md b/multicore-magic/_doc-dir/CHANGES.md new file mode 100644 index 00000000..5b880e21 --- /dev/null +++ b/multicore-magic/_doc-dir/CHANGES.md @@ -0,0 +1,33 @@ +## 2.3.0 + +- Add `copy_as ~padded` for convenient optional padding (@polytypic) +- Add `multicore-magic-dscheck` package and library to help testing with DScheck + (@lyrm, review @polytypic) + +## 2.2.0 + +- Add (unboxed) `Atomic_array` (@polytypic) + +## 2.1.0 + +- Added `instantaneous_domain_index` for the implementation of contention + avoiding data structures. (@polytypic) +- Added `Transparent_atomic` module as a workaround to CSE issues in OCaml 5.0 + and OCaml 5.1 and also to allow more efficient arrays of atomics. (@polytypic) +- Fixed `fenceless_get` to not be subject to CSE. (@polytypic) + +## 2.0.0 + +- Changed the semantics of `copy_as_padded` to not always copy and to not + guarantee that `length_of_padded_array*` works with it. These semantic changes + allow better use of the OCaml allocator to guarantee cache friendly alignment. + (@polytypic) + +## 1.0.1 + +- Ported the library to OCaml 4 (@polytypic) +- License changed to ISC from 0BSD (@tarides) + +## 1.0.0 + +- Initial release (@polytypic) diff --git a/multicore-magic/_doc-dir/LICENSE.md b/multicore-magic/_doc-dir/LICENSE.md new file mode 100644 index 00000000..5da69623 --- /dev/null +++ b/multicore-magic/_doc-dir/LICENSE.md @@ -0,0 +1,13 @@ +Copyright © 2023 Vesa Karvonen + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/multicore-magic/_doc-dir/README.md b/multicore-magic/_doc-dir/README.md new file mode 100644 index 00000000..f0af7454 --- /dev/null +++ b/multicore-magic/_doc-dir/README.md @@ -0,0 +1,8 @@ +[API reference](https://ocaml-multicore.github.io/multicore-magic/doc/multicore-magic/Multicore_magic/index.html) + +# **multicore-magic** — Low-level multicore utilities for OCaml + +This is a library of magic multicore utilities intended for experts for +extracting the best possible performance from multicore OCaml. + +Hopefully future releases of multicore OCaml will make this library obsolete! diff --git a/multicore-magic/index.html b/multicore-magic/index.html new file mode 100644 index 00000000..c6966568 --- /dev/null +++ b/multicore-magic/index.html @@ -0,0 +1,2 @@ + +index (multicore-magic.index)

Package multicore-magic

  • Multicore_magic This is a library of magic multicore utilities intended for experts for extracting the best possible performance from multicore OCaml.

Package info

changes-files
license-files
readme-files
diff --git a/picos/Picos/Computation/Tx/index.html b/picos/Picos/Computation/Tx/index.html new file mode 100644 index 00000000..3b9307be --- /dev/null +++ b/picos/Picos/Computation/Tx/index.html @@ -0,0 +1,7 @@ + +Tx (picos.Picos.Computation.Tx)

Module Computation.Tx

Transactional interface for atomically completing multiple computations.

⚠️ The implementation of this mechanism is designed to avoid making the single computation completing operations, i.e. try_return and try_cancel, slower and to avoid making computations heavier. For this reason the transaction mechanism is only obstruction-free. What this means is that a transaction may be aborted by another transaction or by a single computation manipulating operation.

type 'a computation := 'a t

Destructively substituted alias for Computation.t.

val same : _ computation -> _ computation -> bool

same computation1 computation2 determines whether the two computations are the one and the same.

type t

Represents a transaction.

val create : unit -> t

create () returns a new empty transaction.

val try_return : t -> 'a computation -> 'a -> bool

try_return tx computation value adds the completion of the computation as having returned the given value to the transaction. Returns true in case the computation had not yet been completed and the transaction was still alive. Otherwise returns false which means that transaction was aborted and it is as if none of the completions succesfully added to the transaction have taken place.

val try_cancel : + t -> + 'a computation -> + exn -> + Stdlib.Printexc.raw_backtrace -> + bool

try_cancel tx computation exn bt adds the completion of the computation as having canceled with the given exception and backtrace to the transaction. Returns true in case the computation had not yet been completed and the transaction was still alive. Otherwise returns false which means that transaction was aborted and it is as if none of the completions succesfully added to the transaction have taken place.

val try_commit : t -> bool

try_commit tx attempts to mark the transaction as committed successfully. Returns true in case of success, which means that all the completions added to the transaction have been performed atomically. Otherwise returns false which means that transaction was aborted and it is as if none of the completions succesfully added to the transaction have taken place.

diff --git a/picos/Picos/Computation/index.html b/picos/Picos/Computation/index.html new file mode 100644 index 00000000..ee4c18f7 --- /dev/null +++ b/picos/Picos/Computation/index.html @@ -0,0 +1,47 @@ + +Computation (picos.Picos.Computation)

Module Picos.Computation

A cancelable computation.

A computation basically holds the status, i.e.

  • running,
  • returned, or
  • canceled,

of some sort of computation and most importantly allows anyone to be notified when the status of the computation changes from running to completed.

A hopefully enlightening analogy is that a computation is a kind of single-shot atomic event.

Another hopefully helpful analogy is that a computation is basically like a cancelable promise and a basic non-cancelable promise can be implemented trivially on top of a computation.

To define a computation, one first creates it and then arranges for the computation to be completed by returning a value through it or by canceling it with an exception at some point in the future. There are no restrictions on what it means for a computation to be running. The cancelation status of a computation can be polled or checked explicitly. Observers can also attach triggers to a computation to get a signal when the computation is completed or await the computation.

Here is an example:

run begin fun () ->
+  Flock.join_after @@ fun () ->
+
+  let computation =
+    Computation.create ()
+  in
+
+  let canceler = Flock.fork_as_promise @@ fun () ->
+    Fiber.sleep ~seconds:1.0;
+
+    Computation.cancel computation
+      Exit (Printexc.get_callstack 0)
+  in
+
+  Flock.fork begin fun () ->
+    let rec fib i =
+      Computation.check
+        computation;
+      if i <= 1 then
+        i
+      else
+        fib (i - 1) + fib (i - 2)
+    in
+    Computation.capture computation
+      fib 10;
+
+    Promise.terminate canceler
+  end;
+
+  Computation.await computation
+end

A fiber is always associated with at least a single computation. However, it is possible for multiple fibers to share a single computation and it is also possible for a single fiber to perform multiple computations. Furthermore, the computation associated with a fiber can be changed by the fiber.

Computations are not hierarchical. In other words, computations do not directly implement structured concurrency. However, it is possible to propagate cancelation to implement structured concurrency on top of computations.

Operations on computations are either wait-free or lock-free and designed to avoid starvation and complete in amortized constant time. The properties of operations to complete a computation depend on the properties of actions attached to the triggers.

Interface for creating

type !'a t

Represents a cancelable computation. A computation is either running or has been completed either with a return value or with canceling exception with a backtrace.

ℹ️ Once a computation becomes completed it no longer changes state.

🏎️ A computation that has been completed is a small object that only holds onto the return value or the canceling exception with a backtrace.

⚠️ In the running state a computation may refer to any number of triggers and it is important to make sure that any triggers attached to a computation are detached when they are no longer needed unless the computation has been completed.

val create : ?mode:[ `FIFO | `LIFO ] -> unit -> 'a t

create () creates a new computation in the running state.

The optional mode specifies the order in which triggers attached to the computation will be signaled after the computation has been completed. `FIFO ordering may reduce latency of IO bound computations and is the default. `LIFO may improve thruput of CPU bound computations and be preferable on a work-stealing scheduler, for example.

ℹ️ Typically the creator of a computation object arranges for the computation to be completed by using the capture helper, for example. However, it is possible and safe to race multiple threads of execution to complete a computation.

val returned : 'a -> 'a t

returned value returns a constant computation that has returned the given value.

val finished : unit t

finished is a constant finished computation.

val try_return : 'a t -> 'a -> bool

try_return computation value attempts to complete the computation with the specified value and returns true on success. Otherwise returns false, which means that the computation had already been completed before.

val return : 'a t -> 'a -> unit

return computation value is equivalent to try_return computation value |> ignore.

val try_finish : unit t -> bool

try_finish computation is equivalent to try_return computation ().

val finish : unit t -> unit

finish computation is equivalent to try_finish computation |> ignore.

val try_capture : 'a t -> ('b -> 'a) -> 'b -> bool

try_capture computation fn x calls fn x and tries to complete the computation with the value returned or the exception raised by the call and returns true on success. Otherwise returns false, which means that the computation had already been completed before.

val capture : 'a t -> ('b -> 'a) -> 'b -> unit

capture computation fn x is equivalent to try_capture computation fn x |> ignore.

module Tx : sig ... end

Transactional interface for atomically completing multiple computations.

Interface for canceling

type packed =
  1. | Packed : 'a t -> packed

An existential wrapper for computations.

val try_cancel : 'a t -> exn -> Stdlib.Printexc.raw_backtrace -> bool

try_cancel computation exn bt attempts to mark the computation as canceled with the specified exception and backtrace and returns true on success. Otherwise returns false, which means that the computation had already been completed before.

val cancel : 'a t -> exn -> Stdlib.Printexc.raw_backtrace -> unit

cancel computation exn bt is equivalent to try_cancel computation exn bt |> ignore.

Interface for timeouts

val cancel_after : + 'a t -> + seconds:float -> + exn -> + Stdlib.Printexc.raw_backtrace -> + unit

cancel_after ~seconds computation exn bt arranges to cancel the computation after the specified time with the specified exception and backtrace. Completion of the computation before the specified time effectively cancels the timeout.

ℹ️ The behavior is that cancel_after first checks that seconds is not negative, and then

  • on OCaml 5, cancel_after will perform the Cancel_after effect, and
  • on OCaml 4, cancel_after will call the cancel_after operation of the current handler.
  • raises Invalid_argument

    if seconds is negative or too large as determined by the scheduler.

Interface for polling

val is_running : 'a t -> bool

is_running computation determines whether the computation is in the running state meaning that it has not yet been completed.

val is_canceled : 'a t -> bool

is_canceled computation determines whether the computation is in the canceled state.

val canceled : 'a t -> (exn * Stdlib.Printexc.raw_backtrace) option

canceled computation returns the exception that the computation has been canceled with or returns None in case the computation has not been canceled.

val peek : 'a t -> ('a, exn * Stdlib.Printexc.raw_backtrace) result option

peek computation returns the result of the computation or None in case the computation has not completed.

exception Running

Exception raised by peek_exn when it's used on a still running computation. This should never be surfaced to the user.

val peek_exn : 'a t -> 'a

peek_exn computation returns the result of the computation or raises an exception. It is important to catch the exception. If the computation was cancelled with exception exn then exn is re-raised with its original backtrace.

  • raises Running

    if the computation has not completed.

Interface for awaiting

val try_attach : 'a t -> Trigger.t -> bool

try_attach computation trigger tries to attach the trigger to be signaled on completion of the computation and returns true on success. Otherwise returns false, which means that the computation has already been completed or the trigger has already been signaled.

⚠️ Always detach a trigger after it is no longer needed unless the computation is known to have been completed.

val detach : 'a t -> Trigger.t -> unit

detach computation trigger signals the trigger and detaches it from the computation.

🏎️ The try_attach and detach operations essentially implement a lock-free bag. While not formally wait-free, the implementation is designed to avoid starvation by making sure that any potentially expensive operations are performed cooperatively.

val await : 'a t -> 'a

await computation waits for the computation to complete and either returns the value of the completed computation or raises the exception the computation was canceled with.

ℹ️ If the computation has already completed, then await returns or raises immediately without performing any effects.

val wait : _ t -> unit

wait computation waits for the computation to complete.

Interface for propagating cancelation

val canceler : from:_ t -> into:_ t -> Trigger.t

canceler ~from ~into creates a trigger that propagates cancelation from one computation into another on signal. The returned trigger is not attached to any computation.

The returned trigger is usually attached to the computation from which cancelation is to be propagated and the trigger should usually also be detached after it is no longer needed.

The intended use case of canceler is as a low level building block of structured concurrency mechanisms. Picos does not require concurrent programming models to be hierarchical or structured.

⚠️ The returned trigger will be in the awaiting state, which means that it is an error to call Trigger.await or Trigger.on_signal on it.

val attach_canceler : from:_ t -> into:_ t -> Trigger.t

attach_canceler ~from ~into tries to attach a canceler to the computation from to propagate cancelation to the computation into and returns the canceler when successful. If the computation from has already been canceled, the exception that from was canceled with will be raised.

  • raises Invalid_argument

    if the from computation has already returned.

Interface for schedulers

type Stdlib.Effect.t += private
  1. | Cancel_after : {
    1. seconds : float;
      (*

      Guaranteed to be non-negative.

      *)
    2. exn : exn;
    3. bt : Stdlib.Printexc.raw_backtrace;
    4. computation : 'a t;
    } -> unit Stdlib.Effect.t

Schedulers must handle the Cancel_after effect to implement the behavior of cancel_after.

The scheduler should typically attach a trigger to the computation passed with the effect and arrange the timeout to be canceled upon signal to avoid space leaks.

The scheduler should measure time using a monotonic clock.

In case the fiber permits propagation of cancelation and the computation associated with the fiber has been canceled the scheduler is free to discontinue the fiber before setting up the timeout.

If the fiber is continued normally, i.e. without raising an exception, the scheduler should guarantee that the cancelation will be delivered eventually.

The scheduler is free to choose which ready fiber to resume next.

val with_action : + ?mode:[ `FIFO | `LIFO ] -> + 'x -> + 'y -> + (Trigger.t -> 'x -> 'y -> unit) -> + 'a t

with_action x y resume is equivalent to

let computation = create () in
+let trigger =
+  Trigger.from_action x y resume in
+let _ : bool =
+  try_attach computation trigger in
+computation

⚠️ The same warnings as with Trigger.from_action apply.

  • alert handler This is an escape hatch for experts implementing schedulers or structured concurrency mechanisms. If you know what you are doing, use [@alert "-handler"].

Design rationale

The main feature of the computation concept is that anyone can efficiently attach triggers to a computation to get notified when the status of the computation changes from running to completed and can also efficiently detach such triggers in case getting a notification is no longer necessary. This allows the status change to be propagated omnidirectionally and is what makes computations able to support a variety of purposes such as the implementation of structured concurrency.

The computation concept can be seen as a kind of single-shot atomic event that is a generalization of both a cancelation Context or token and of a promise. Unlike a typical promise mechanism, a computation can be canceled. Unlike a typical cancelation mechanism, a computation can and should also be completed in case it is not canceled. This promotes proper scoping of computations and resource cleanup at completion, which is how the design evolved from a more traditional cancelation context design.

Every fiber is associated with a computation. Being able to return a value through the computation means that no separate promise is necessarily required to hold the result of a fiber. On the other hand, multiple fibers may share a single computation. This allows multiple fibers to be canceled efficiently through a single atomic update. In other words, the design allows various higher level patterns to be implemented efficiently.

Instead of directly implementing a hierarchy of computations, the design allows attaching triggers to computations and a special trigger constructor is provided for propagating cancelation. This helps to keep the implementation lean, i.e. not substantially heavier than a typical promise implementation.

Finally, just like with Trigger.Await, a key idea is that the handler of Computation.Cancel_after does not need to run arbitrary user defined code. The action of any trigger attached to a computation either comes from some scheduler calling Trigger.on_signal or from Computation.canceler.

diff --git a/picos/Picos/Fiber/FLS/index.html b/picos/Picos/Fiber/FLS/index.html new file mode 100644 index 00000000..4443f2e7 --- /dev/null +++ b/picos/Picos/Fiber/FLS/index.html @@ -0,0 +1,2 @@ + +FLS (picos.Picos.Fiber.FLS)

Module Fiber.FLS

Fiber local storage

Fiber local storage is intended for use as a low overhead storage mechanism for fiber extensions. For example, one might associate a priority value with each fiber for a scheduler that uses a priority queue or one might use FLS to store unique id values for fibers.

type fiber := t

Destructively substituted alias for Fiber.t.

type 'a t

Represents a key for storing values of type 'a in storage associated with fibers.

val create : unit -> 'a t

new_key initial allocates a new key for associating values in storage associated with fibers. The initial value for every fiber is either the given Constant or is Computed with the given function. If the initial value is a constant, no value needs to be stored unless the value is explicitly updated.

⚠️ New keys should not be created dynamically.

exception Not_set

Raised by get_exn in case value has not been set.

val get_exn : fiber -> 'a t -> 'a

get_exn fiber key returns the value associated with the key in the storage associated with the fiber or raises Not_set using raise_notrace.

⚠️ It is only safe to call get_exn from the fiber itself or when the fiber is known not to be running.

val get : fiber -> 'a t -> default:'a -> 'a

get fiber key ~default returns the value associated with the key in the storage associated with the fiber or the default value.

⚠️ It is only safe to call get from the fiber itself or when the fiber is known not to be running.

val set : fiber -> 'a t -> 'a -> unit

set fiber key value sets the value associated with the key to the given value in the storage associated with the fiber.

⚠️ It is only safe to call set from the fiber itself or when the fiber is known not to be running.

diff --git a/picos/Picos/Fiber/Maybe/index.html b/picos/Picos/Fiber/Maybe/index.html new file mode 100644 index 00000000..4eb5f9a0 --- /dev/null +++ b/picos/Picos/Fiber/Maybe/index.html @@ -0,0 +1,2 @@ + +Maybe (picos.Picos.Fiber.Maybe)

Module Fiber.Maybe

An unboxed optional fiber.

type t

Either a fiber or nothing.

val nothing : t

Not a fiber.

val of_fiber : fiber -> t

of_fiber fiber casts the fiber into an optional fiber.

🏎️ This performs no allocations.

val to_fiber : t -> fiber

to_fiber casts the optional fiber to a fiber.

  • raises Invalid_argument

    in case the optional fiber is nothing.

val current_if : bool option -> t

current_if checked returns nothing in case checked is Some false and otherwise of_fiber (Fiber.current ()).

val current_and_check_if : bool option -> t

current_check_if checked returns nothing in case checked is Some false and otherwise of_fiber (Fiber.current ()) and also calls Fiber.check on the fiber.

val or_current : t -> t

or_current maybe returns of_fiber (Fiber.current ()) in case maybe is nothing and otherwise returns maybe.

val to_fiber_or_current : t -> fiber

to_fiber_or_current maybe returns Fiber.current () in case maybe is nothing and otherwise returns the fiber that maybe was cast from.

val check : t -> unit

check maybe returns immediately if maybe is nothing and otherwise calls Fiber.check on the fiber.

val equal : t -> t -> bool

equal l r determines whether l and r are maybe equal. Specifically, if either l or r or both is nothing, then they are considered (maybe) equal. Otherwise l and r are compared for physical equality.

val unequal : t -> t -> bool

equal l r determines whether l and r are maybe unequal. Specifically, if either l or r or both is nothing, then they are considered (maybe) unequal. Otherwise l and r are compared for physical equality.

Design rationale

The fiber identity is often needed only for the purpose of dynamically checking against programming errors. Unfortunately it can be relative expensive to obtain the current fiber.

As a data point, in a benchmark that increments an int ref protected by a mutex, obtaining the fiber identity for the lock and unlock operations — that only need it for error checking purposes — roughly tripled the cost of an increment on a machine.

Using GADTs internally allows an optional fiber to be provided without adding overhead to operations on non-optional fibers and allows optional fibers to used without allocations at a very low cost.

diff --git a/picos/Picos/Fiber/index.html b/picos/Picos/Fiber/index.html new file mode 100644 index 00000000..9ceebd7d --- /dev/null +++ b/picos/Picos/Fiber/index.html @@ -0,0 +1,42 @@ + +Fiber (picos.Picos.Fiber)

Module Picos.Fiber

An independent thread of execution.

A fiber corresponds to an independent thread of execution. Fibers are created by schedulers in response to Spawn effects. A fiber is associated with a computation and either forbids or permits the scheduler from propagating cancelation when the fiber performs effects. A fiber also has an associated fiber local storage.

⚠️ Many operations on fibers can only be called safely from the fiber itself, because those operations are neither concurrency nor parallelism safe. Such operations can be safely called from a handler in a scheduler when it is handling an effect performed by the fiber. In particular, a scheduler can safely check whether the fiber has_forbidden cancelation and may access the FLS of the fiber.

Interface for rescheduling

val yield : unit -> unit

yield () asks the current fiber to be rescheduled.

ℹ️ The behavior is that

  • on OCaml 5, yield perform the Yield effect, and
  • on OCaml 4, yield will call the yield operation of the current handler.
val sleep : seconds:float -> unit

sleep ~seconds suspends the current fiber for the specified number of seconds.

Interface for current fiber

type t

Represents a fiber or an independent thread of execution.

⚠️ Unlike with most other concepts of Picos, operations on fibers are typically not concurrency or parallelism safe, because the fiber is considered to be owned by a single thread of execution.

type fiber := t

Type alias for Fiber.t within this signature.

val current : unit -> t

current () returns the current fiber.

⚠️ Extra care should be taken when storing the fiber object in any shared data structure, because, aside from checking whether two fibers are equal, it is generally unsafe to perform any operations on foreign fibers.

ℹ️ The behavior is that

  • on OCaml 5, current performs the Current effect, and
  • on OCaml 4, current will call the current operation of the current handler.

⚠️ The current operation must always resume the fiber without propagating cancelation. A scheduler may, of course, decide to reschedule the current fiber to be resumed later.

Because the scheduler does not discontinue a fiber calling current, it is recommended that the caller checks the cancelation status at the next convenient moment to do so.

val has_forbidden : t -> bool

has_forbidden fiber determines whether the fiber forbids or permits the scheduler from propagating cancelation to it.

ℹ️ This is mostly useful in the effect handlers of schedulers.

⚠️ There is no "reference count" of how many times a fiber has forbidden or permitted propagation of cancelation. Calls to forbid and permit directly change a single boolean flag.

⚠️ It is only safe to call has_forbidden from the fiber itself.

val forbid : t -> (unit -> 'a) -> 'a

forbid fiber thunk tells the scheduler that cancelation must not be propagated to the fiber during the execution of thunk.

The main use case of forbid is the implementation of concurrent abstractions that may have to await for something, or may need to perform other effects, and must not be canceled while doing so. For example, the wait operation on a condition variable typically reacquires the associated mutex before returning, which may require awaiting for the owner of the mutex to release it.

ℹ️ forbid does not prevent the fiber or the associated computation from being canceled. It only tells the scheduler not to propagate cancelation to the fiber when it performs effects.

⚠️ It is only safe to call forbid from the fiber itself.

val permit : t -> (unit -> 'a) -> 'a

permit fiber thunk tells the scheduler that cancelation may be propagated to the fiber during the execution of thunk.

It is possible to spawn a fiber with cancelation forbidden, which means that cancelation won't be propagated to fiber unless it is explicitly permitted by the fiber at some point.

⚠️ It is only safe to call permit from the fiber itself.

val is_canceled : t -> bool

is_canceled fiber is equivalent to

not (Fiber.has_forbidden fiber) &&
+let (Packed computation) =
+  Fiber.get_computation fiber
+in
+Computation.is_canceled computation

ℹ️ This is mostly useful in the effect handlers of schedulers.

⚠️ It is only safe to call is_canceled from the fiber itself.

val canceled : t -> (exn * Stdlib.Printexc.raw_backtrace) option

canceled fiber is equivalent to:

if Fiber.has_forbidden fiber then
+  None
+else
+  let (Packed computation) =
+    Fiber.get_computation fiber
+  in
+  Computation.canceled computation

ℹ️ This is mostly useful in the effect handlers of schedulers.

⚠️ It is only safe to call canceled from the fiber itself.

val check : t -> unit

check fiber is equivalent to:

if not (Fiber.has_forbidden fiber) then
+  let (Packed computation) =
+    Fiber.get_computation fiber
+  in
+  Computation.check computation

ℹ️ This is mostly useful for periodically polling the cancelation status during CPU intensive work.

⚠️ It is only safe to call check from the fiber itself.

val exchange : t -> forbid:bool -> bool

exchange fiber ~forbid sets the bit that tells the scheduler whether to propagate cancelation or not and returns the previous state.

val set : t -> forbid:bool -> unit

set fiber ~forbid sets the bit that tells the scheduler whether to propagate cancelation or not.

module FLS : sig ... end

Fiber local storage

Interface for spawning

val create_packed : forbid:bool -> Computation.packed -> t

create_packed ~forbid packed creates a new fiber record.

val create : forbid:bool -> 'a Computation.t -> t

create ~forbid computation is equivalent to create_packed ~forbid (Computation.Packed computation).

val spawn : t -> (t -> unit) -> unit

spawn fiber main starts a new fiber by performing the Spawn effect.

⚠️ Fiber records must be unique and the caller of spawn must make sure that a specific fiber record is not reused. Failure to ensure that fiber records are unique will break concurrent abstractions written on top the the Picos interface.

⚠️ If the main function raises an exception it is considered a fatal error. A fatal error should effectively either directly exit the program or stop the entire scheduler, without discontinuing existing fibers, and force the invocations of the scheduler on all domains to exit. What this means is that the caller of spawn should ideally arrange for any exception to be handled by main, but, in case that is not practical, it is also possible to allow an exception to propagate out of main, which is then guaranteed to, one way or the other, to stop the entire program. It is not possible to recover from a fatal error.

ℹ️ The behavior is that

  • on OCaml 5, spawn performs the Spawn effect, and
  • on OCaml 4, spawn will call the spawn operation of the current handler.

Interface for structuring

val get_computation : t -> Computation.packed

get_computation fiber returns the computation that the fiber is currently associated with.

val set_computation : t -> Computation.packed -> unit

set_computation fiber packed associates the fiber with the specified computation.

Interface for foreign fiber

val equal : t -> t -> bool

equal fiber1 fiber2 is physical equality for fibers, i.e. it determines whether fiber1 and fiber2 are one and the same fiber.

ℹ️ One use case of equal is in the implementation of concurrent primitives like mutexes where it makes sense to check that acquire and release operations are performed by the same fiber.

module Maybe : sig ... end

An unboxed optional fiber.

Interface for schedulers

val try_suspend : + t -> + Trigger.t -> + 'x -> + 'y -> + (Trigger.t -> 'x -> 'y -> unit) -> + bool

try_suspend fiber trigger x y resume tries to suspend the fiber to await for the trigger to be signaled. If the result is false, then the trigger is guaranteed to be in the signaled state and the fiber should be eventually resumed. If the result is true, then the fiber was suspended, meaning that the trigger will have had the resume action attached to it and the trigger has potentially been attached to the computation of the fiber.

val unsuspend : t -> Trigger.t -> bool

unsuspend fiber trigger makes sure that the trigger will not be attached to the computation of the fiber. Returns false in case the fiber has been canceled and propagation of cancelation is not forbidden. Otherwise returns true.

⚠️ The trigger must be in the signaled state!

val resume : + t -> + ((exn * Stdlib.Printexc.raw_backtrace) option, 'r) + Stdlib.Effect.Deep.continuation -> + 'r

resume fiber k is equivalent to Effect.Deep.continue k (Fiber.canceled t).

val resume_with : + t -> + ((exn * Stdlib.Printexc.raw_backtrace) option, 'b) + Stdlib.Effect.Shallow.continuation -> + ('b, 'r) Stdlib.Effect.Shallow.handler -> + 'r

resume_with fiber k h is equivalent to Effect.Shallow.continue_with k (Fiber.canceled t) h.

val continue : t -> ('v, 'r) Stdlib.Effect.Deep.continuation -> 'v -> 'r

continue fiber k v is equivalent to:

match Fiber.canceled fiber with
+| None -> Effect.Deep.continue k v
+| Some (exn, bt) ->
+  Effect.Deep.discontinue_with_backtrace k exn bt
val continue_with : + t -> + ('v, 'b) Stdlib.Effect.Shallow.continuation -> + 'v -> + ('b, 'r) Stdlib.Effect.Shallow.handler -> + 'r

continue_with fiber k v h is equivalent to:

match Fiber.canceled fiber with
+| None -> Effect.Shallow.continue_with k v h
+| Some (exn, bt) ->
+  Effect.Shallow.discontinue_with_backtrace k exn bt h
type Stdlib.Effect.t += private
  1. | Current : t Stdlib.Effect.t

Schedulers must handle the Current effect to implement the behavior of current.

⚠️ The scheduler must eventually resume the fiber without propagating cancelation. This is necessary to allow a fiber to control the propagation of cancelation through the fiber.

The scheduler is free to choose which ready fiber to resume next. However, in typical use cases of current it makes sense to give priority to the fiber performing Current, but this is not required.

type Stdlib.Effect.t += private
  1. | Yield : unit Stdlib.Effect.t

Schedulers must handle the Yield effect to implement the behavior of yield.

In case the fiber permits propagation of cancelation and the computation associated with the fiber has been canceled the scheduler is free to discontinue the fiber immediately.

The scheduler should give priority to running other ready fibers before resuming the fiber performing Yield. A scheduler that always immediately resumes the fiber performing Yield may prevent an otherwise valid program from making progress.

type Stdlib.Effect.t += private
  1. | Spawn : {
    1. fiber : t;
    2. main : t -> unit;
    } -> unit Stdlib.Effect.t

Schedulers must handle the Spawn effect to implement the behavior of spawn.

In case the fiber permits propagation of cancelation and the computation associated with the fiber has been canceled the scheduler is free to discontinue the fiber immediately before spawning new fibers.

The scheduler is free to run the newly created fiber on any domain and decide which fiber to give priority to.

⚠️ The scheduler should guarantee that, when Spawn returns normally, the given main will eventually be called by the scheduler and, when Spawn raises an exception, the main will not be called. In other words, Spawn should check cancelation just once and be all or nothing. Furthermore, in case a newly spawned fiber is canceled before its main is called, the scheduler must still call the main. This allows a program to ensure, i.e. keep track of, that all fibers it spawns are terminated properly and any resources transmitted to spawned fibers will be disposed properly.

Design rationale

The idea is that fibers correspond 1-to-1 with independent threads of execution. This allows a fiber to non-atomically store state related to a thread of execution.

The status of whether propagation of cancelation is forbidden or permitted could be stored in the fiber local storage. The justification for storing it directly with the fiber is that the implementation of some key synchronization and communication mechanisms, such as condition variables, requires the capability.

No integer fiber id is provided by default. It would seem that for most intents and purposes the identity of the fiber is sufficient. Fiber local storage can be used to implement a fiber id or e.g. a fiber hash.

The fiber local storage is designed for the purpose of extending fibers and to be as fast as possible. It is not intended for application programming.

Yield is provided as a separate effect to specifically communicate the intent that the current fiber should be rescheduled. This allows all the other effect handlers more freedom in choosing which fiber to schedule next.

diff --git a/picos/Picos/Handler/index.html b/picos/Picos/Handler/index.html new file mode 100644 index 00000000..607be7f6 --- /dev/null +++ b/picos/Picos/Handler/index.html @@ -0,0 +1,7 @@ + +Handler (picos.Picos.Handler)

Module Picos.Handler

Handler for the effects based operations of Picos for OCaml 4.

type 'c t = {
  1. current : 'c -> Fiber.t;
  2. spawn : 'c -> Fiber.t -> (Fiber.t -> unit) -> unit;
  3. yield : 'c -> unit;
  4. cancel_after : 'a. 'c -> + 'a Computation.t -> + seconds:float -> + exn -> + Stdlib.Printexc.raw_backtrace -> + unit;
  5. await : 'c -> Trigger.t -> (exn * Stdlib.Printexc.raw_backtrace) option;
}

A record of implementations of the primitive effects based operations of Picos. The operations take a context of type 'c as an argument.

val using : 'c t -> 'c -> (Fiber.t -> unit) -> unit

using handler context main sets the handler and the context for the handler of the primitive effects based operations of Picos while running main.

ℹ️ The behavior is that

  • on OCaml 4, using stores the handler in TLS, which allows the operations to be accessed during the execution of the thunk, and
  • on OCaml 5, using runs thunk with a deep effect handler that delegates to the operations of the handler.

⚠️ While this works on OCaml 5, you usually want to use a scheduler that implements an effect handler directly, because that is likely to perform better.

diff --git a/picos/Picos/Trigger/index.html b/picos/Picos/Trigger/index.html new file mode 100644 index 00000000..6edbd6dd --- /dev/null +++ b/picos/Picos/Trigger/index.html @@ -0,0 +1,18 @@ + +Trigger (picos.Picos.Trigger)

Module Picos.Trigger

Ability to await for a signal.

To suspend and later resume the current thread of execution, one can create a trigger, arrange signal to be called on it, and await for the call.

Here is a simple example:

run begin fun () ->
+  Flock.join_after @@ fun () ->
+
+  let trigger = Trigger.create () in
+
+  Flock.fork begin fun () ->
+    Trigger.signal trigger
+  end;
+
+  match Trigger.await trigger with
+  | None ->
+    (* We were resumed normally. *)
+    ()
+  | Some (exn, bt) ->
+    (* We were canceled. *)
+    Printexc.raise_with_backtrace exn bt
+end

⚠️ Typically we need to cleanup after await, but in the above example we didn't insert the trigger into any data structure nor did we attach the trigger to any computation.

All operations on triggers are wait-free, with the obvious exception of await. The signal operation inherits the properties of the action attached with on_signal to the trigger.

Interface for suspending

type t

Represents a trigger. A trigger can be in one of three states: initial, awaiting, or signaled.

ℹ️ Once a trigger becomes signaled it no longer changes state.

🏎️ A trigger in the initial and signaled states is a tiny object that does not hold onto any other objects.

val create : unit -> t

create () allocates a new trigger in the initial state.

val is_signaled : t -> bool

is_signaled trigger determines whether the trigger is in the signaled state.

This can be useful, for example, when a trigger is being inserted to multiple locations and might be signaled concurrently while doing so. In such a case one can periodically check with is_signaled trigger whether it makes sense to continue.

ℹ️ Computation.try_attach already checks that the trigger being inserted has not been signaled so when attaching a trigger to multiple computations there is no need to separately check with is_signaled.

val await : t -> (exn * Stdlib.Printexc.raw_backtrace) option

await trigger waits for the trigger to be signaled.

The return value is None in case the trigger has been signaled and the fiber was resumed normally. Otherwise the return value is Some (exn, bt), which indicates that the fiber has been canceled and the caller should raise the exception. In either case the caller is responsible for cleaning up. Usually this means making sure that no references to the trigger remain to avoid space leaks.

⚠️ As a rule of thumb, if you inserted the trigger to some data structure or attached it to some computation, then you are responsible for removing and detaching the trigger after await.

ℹ️ A trigger in the signaled state only takes a small constant amount of memory. Make sure that it is not possible for a program to accumulate unbounded numbers of signaled triggers under any circumstance.

⚠️ Only the owner or creator of a trigger may call await. It is considered an error to make multiple calls to await.

ℹ️ The behavior is that, unless await can return immediately,

  • on OCaml 5, await will perform the Await effect, and
  • on OCaml 4, await will call the await operation of the current handler.
  • raises Invalid_argument

    if the trigger was in the awaiting state, which means that multiple concurrent calls of await are being made.

Interface for resuming

val signal : t -> unit

signal trigger puts the trigger into the signaled state and calls the resume action, if any, attached using on_signal.

The intention is that calling signal trigger guarantees that any fiber awaiting the trigger will be resumed. However, when and whether a fiber having called await will be resumed normally or as canceled is determined by the scheduler that handles the Await effect.

ℹ️ Note that under normal circumstances, signal should never raise an exception. If an exception is raised by signal, it means that the handler of Await has a bug or some catastrophic failure has occurred.

⚠️ Do not call signal from an effect handler in a scheduler.

Interface for schedulers

val is_initial : t -> bool

is_initial trigger determines whether the trigger is in the initial or in the signaled state.

ℹ️ Consider using is_signaled instead of is_initial as in some contexts a trigger might reasonably be either in the initial or the awaiting state depending on the order in which things are being done.

  • raises Invalid_argument

    if the trigger was in the awaiting state.

val on_signal : t -> 'x -> 'y -> (t -> 'x -> 'y -> unit) -> bool

on_signal trigger x y resume attempts to attach the resume action to the trigger and transition the trigger to the awaiting state.

The return value is true in case the action was attached successfully. Otherwise the return value is false, which means that the trigger was already in the signaled state.

⚠️ The action that you attach to a trigger must be safe to call from any context that might end up signaling the trigger directly or indirectly through propagation. Unless you know, then you should assume that the resume action might be called from a different domain running in parallel with neither effect nor exception handlers and that if the attached action doesn't return the system may deadlock or if the action doesn't return quickly it may cause performance issues.

⚠️ It is considered an error to make multiple calls to on_signal with a specific trigger.

  • raises Invalid_argument

    if the trigger was in the awaiting state, which means that either the owner or creator of the trigger made concurrent calls to await or the handler called on_signal more than once.

  • alert handler Only a scheduler should call this in the handler of the Await effect to attach the scheduler specific resume action to the trigger. Annotate your effect handling function with [@alert "-handler"].
val from_action : 'x -> 'y -> (t -> 'x -> 'y -> unit) -> t

from_action x y resume is equivalent to let t = create () in assert (on_signal t x y resume); t.

⚠️ The action that you attach to a trigger must be safe to call from any context that might end up signaling the trigger directly or indirectly through propagation. Unless you know, then you should assume that the resume action might be called from a different domain running in parallel with neither effect nor exception handlers and that if the attached action doesn't return the system may deadlock or if the action doesn't return quickly it may cause performance issues.

⚠️ The returned trigger will be in the awaiting state, which means that it is an error to call await, on_signal, or dispose on it.

  • alert handler This is an escape hatch for experts implementing schedulers or structured concurrency mechanisms. If you know what you are doing, use [@alert "-handler"].
val dispose : t -> unit

dispose trigger transition the trigger from the initial state to the signaled state.

🚦 The intended use case of dispose is for use from the handler of Await to ensure that the trigger has been put to the signaled state after await returns.

  • raises Invalid_argument

    if the trigger was in the awaiting state.

type Stdlib.Effect.t += private
  1. | Await : t -> (exn * Stdlib.Printexc.raw_backtrace) option Stdlib.Effect.t

Schedulers must handle the Await effect to implement the behavior of await.

In case the fiber permits propagation of cancelation, the trigger must be attached to the computation of the fiber for the duration of suspending the fiber by the scheduler.

Typically the scheduler calls try_suspend, which in turn calls on_signal, to attach a scheduler specific resume action to the trigger. The scheduler must guarantee that the fiber will be resumed after signal has been called on the trigger.

Whether being resumed due to cancelation or not, the trigger must be either signaled outside of the effect handler, or disposed by the effect handler, before resuming the fiber.

In case the fiber permits propagation of cancelation and the computation associated with the fiber has been canceled the scheduler is free to continue the fiber immediately with the cancelation exception after disposing the trigger.

⚠️ A scheduler must not discontinue, i.e. raise an exception to, the fiber as a response to Await.

The scheduler is free to choose which ready fiber to resume next.

Design rationale

A key idea behind this design is that the handler for Await does not need to run arbitrary user defined code while suspending a fiber: the handler calls on_signal by itself. This should make it easier to get both the handler and the user code correct.

Another key idea is that the signal operation provides no feedback as to the outcome regarding cancelation. Calling signal merely guarantees that the caller of await will return. This means that the point at which cancelation must be determined can be as late as possible. A scheduler can check the cancelation status just before calling continue and it is, of course, possible to check the cancelation status earlier. This allows maximal flexibility for the handler of Await.

The consequence of this is that the only place to handle cancelation is at the point of await. This makes the design simpler and should make it easier for the user to get the handling of cancelation right. A minor detail is that await returns an option instead of raising an exception. The reason for this is that matching against an option is slightly faster than setting up an exception handler. Returning an option also clearly communicates the two different cases to handle.

On the other hand, the trigger mechanism does not have a way to specify a user-defined callback to perform cancelation immediately before the fiber is resumed. Such an immediately called callback could be useful for e.g. canceling an underlying IO request. One justification for not having such a callback is that cancelation is allowed to take place from outside of the scheduler, i.e. from another system level thread, and, in such a case, the callback could not be called immediately. Instead, the scheduler is free to choose how to schedule canceled and continued fibers and, assuming that fibers can be trusted, a scheduler may give priority to canceled fibers.

This design also separates the allocation of the atomic state for the trigger, or create, from await, and allows the state to be polled using is_signaled before calling await. This is particularly useful when the trigger might need to be inserted to multiple places and be signaled in parallel before the call of await.

No mechanism is provided to communicate any result with the signal. That can be done outside of the mechanism and is often not needed. This simplifies the design.

Once signal has been called, a trigger no longer refers to any other object and takes just two words of memory. This e.g. allows lazy removal of triggers, assuming the number of attached triggers can be bounded, because nothing except the trigger itself would be leaked.

To further understand the problem domain, in this design, in a suspend-resume scenario, there are three distinct pieces of state:

  1. The state of shared data structure(s) used for communication and / or synchronization.
  2. The state of the trigger.
  3. The cancelation status of the fiber.

The trigger and cancelation status are both updated independently and atomically through code in this interface. The key requirement left for the user is to make sure that the state of the shared data structure is updated correctly independently of what await returns. So, for example, a mutex implementation must check, after getting Some (exn, bt), what the state of the mutex is and how it should be updated.

diff --git a/picos/Picos/index.html b/picos/Picos/index.html new file mode 100644 index 00000000..ead6eacb --- /dev/null +++ b/picos/Picos/index.html @@ -0,0 +1,6 @@ + +Picos (picos.Picos)

Module Picos

A systems programming interface between effects based schedulers and concurrent abstractions.

This is essentially an interface between schedulers and concurrent abstractions that need to communicate with a scheduler. Perhaps an enlightening analogy is to say that this is the POSIX of effects based schedulers.

ℹ️ Picos, i.e. this module, is not intended to be an application level concurrent programming library or framework. If you are looking for a library or framework for programming concurrent applications, then this module is probably not what you are looking for.

The architecture of Picos

The core concepts of Picos are

  • Trigger — ability to await for a signal,
  • Computation — a cancelable computation, and
  • Fiber — an independent thread of execution,

that are implemented in terms of the effects

that can be used to implement many kinds of higher level concurrent programming facilities.

Understanding cancelation

A central idea of Picos is to provide a collection of building blocks for parallelism safe cancelation. Consider the following characteristic example:

Mutex.protect mutex begin fun () ->
+  while true do
+    Condition.wait condition mutex
+  done
+end

Assume that the fiber executing the above computation might be canceled, at any point, by another fiber running in parallel. How could that be done both effectively and safely?

  • To be effective, cancelation should take effect as soon as possible. In this case, cancelation should take effect even during the Mutex.lock inside Mutex.protect and the Condition.wait operations when the fiber might be in a suspended state awaiting for a signal to continue.
  • To be safe, cancelation should not leave the program in an invalid state or cause the program to leak memory. In this case, the ownership of the mutex must be transferred to the next fiber or be left unlocked and no references to unused objects must be left in the mutex or the condition variable.

Picos allows Mutex and Condition to be implemented such that cancelation may safely take effect at or during calls to Mutex.lock and Condition.wait.

Cancelation in Picos

The Fiber concept in Picos corresponds to an independent thread of execution. A fiber may explicitly forbid or permit the scheduler from propagating cancelation to it. This is important for the implementation of some key concurrent abstractions such as condition variables, where it is necessary to forbid cancelation when the associated mutex is reacquired.

Each fiber has an associated Computation at all times. A computation is something that needs to be completed either by returning a value through it or by canceling it with an exception. To cancel a fiber one cancels the computation associated with the fiber or any computation whose cancelation is propagated to the computation associated with the fiber.

Before a computation has been completed, it is also possible to attach a Trigger to the computation and also to later detach the trigger from the computation. A trigger attached to a computation is signaled as the computation is completed.

The Trigger concept in Picos is what allows a fiber to be suspended and later resumed. A fiber can create a trigger, add it to any shared data structure(s), and await for the trigger to be signaled. The await operation, which is implemented by the scheduler, also, in case the fiber permits cancelation, attaches the trigger to the computation of the fiber when it suspends the fiber. This is what allows a fiber to be resumed via cancelation of the computation.

The return value of await tells whether the fiber was resumed normally or due to being canceled and the caller then needs to properly handle either case. After being canceled, depending on the concurrent abstraction being implemented, the caller might need to e.g. remove references to the trigger from the shared data structures, cancel asynchronous IO operations, or transfer ownership of a mutex to the next fiber in the queue of the mutex.

Modules reference

For the examples in this document, we first open the Picos module

open Picos

as well as the Picos_std_structured library,

open Picos_std_structured

which we will be using for managing fibers in some of the examples, and define a simple scheduler on OCaml 4

let run main = Picos_mux_thread.run main

using the basic thread based scheduler and on OCaml 5

let run main = Picos_mux_random.run_on ~n_domains:2 main

using the randomized effects based scheduler that come with Picos as samples.

Core modules

module Trigger : sig ... end

Ability to await for a signal.

module Computation : sig ... end

A cancelable computation.

module Fiber : sig ... end

An independent thread of execution.

module Handler : sig ... end

Handler for the effects based operations of Picos for OCaml 4.

diff --git a/picos/Picos__/index.html b/picos/Picos__/index.html new file mode 100644 index 00000000..392fbd79 --- /dev/null +++ b/picos/Picos__/index.html @@ -0,0 +1,2 @@ + +Picos__ (picos.Picos__)

Module Picos__

This module is hidden.

diff --git a/picos/Picos__Intf/index.html b/picos/Picos__Intf/index.html new file mode 100644 index 00000000..3ae61885 --- /dev/null +++ b/picos/Picos__Intf/index.html @@ -0,0 +1,2 @@ + +Picos__Intf (picos.Picos__Intf)

Module Picos__Intf

This module is hidden.

diff --git a/picos/Picos_domain/DLS/index.html b/picos/Picos_domain/DLS/index.html new file mode 100644 index 00000000..78f2c7cf --- /dev/null +++ b/picos/Picos_domain/DLS/index.html @@ -0,0 +1,2 @@ + +DLS (picos.Picos_domain.DLS)

Module Picos_domain.DLS

Domain-local storage for Picos.

ℹ️ On OCaml 4 there is always only a single domain.

type 'a key

Represents a key for storing values of type 'a in storage associated with domains.

val new_key : (unit -> 'a) -> 'a key

new_key compute allocates a new key for associating values in storage associated with domains. The initial value for each domain is computed by calling the given function if the key is read before it has been written. The compute function might be called multiple times per domain, but only one result will be used.

val get : 'a key -> 'a

get key returns the value associated with the key in the storage associated with the current domain.

val set : 'a key -> 'a -> unit

set key value sets the value associated with the key in the storage associated with the current domain.

diff --git a/picos/Picos_domain/index.html b/picos/Picos_domain/index.html new file mode 100644 index 00000000..f602f54d --- /dev/null +++ b/picos/Picos_domain/index.html @@ -0,0 +1,2 @@ + +Picos_domain (picos.Picos_domain)

Module Picos_domain

Minimalistic domain API available both on OCaml 5 and on OCaml 4.

ℹ️ On OCaml 4 there is always only a single domain.

val at_exit : (unit -> unit) -> unit

at_exit action registers action to be called when the current domain exits.

On OCaml 5 this calls Domain.at_exit. On OCaml 4 this calls Stdlib.at_exit.

recommended_domain_count () returns 1 on OCaml 4 and calls Domain.recommended_domain_count on OCaml 5.

val is_main_domain : unit -> bool

is_main_domain () returns true on OCaml 4 and calls Domain.is_main_domain on OCaml 5.

module DLS : sig ... end

Domain-local storage for Picos.

diff --git a/picos/Picos_thread/TLS/index.html b/picos/Picos_thread/TLS/index.html new file mode 100644 index 00000000..699869db --- /dev/null +++ b/picos/Picos_thread/TLS/index.html @@ -0,0 +1,2 @@ + +TLS (picos.Picos_thread.TLS)

Module Picos_thread.TLS

Thread-local storage.

Note that here "thread" refers to system level threads rather than fibers or domains. In case a system level thread implementation, i.e. the threads.posix library, is not available, this will use Picos_domain.DLS.

type 'a t

Represents a key for associating values with threads.

val create : unit -> 'a t

create () allocates a new key for associating values with threads.

⚠️ Keys should not be created dynamically as each key will potentially increase the space taken by every thread.

exception Not_set

Exception raised by get_exn when no value is associated with the specified key for the current thread.

val get_exn : 'a t -> 'a

get_exn key returns the value associated with the specified key for the current thread or raises Not_set in case no value has been set for the key.

⚠️ The Not_set exception is raised with no backtrace. Always catch the exception unless it is known that a value has been set.

val set : 'a t -> 'a -> unit

set key value associates the value with the specified key for the current thread.

diff --git a/picos/Picos_thread/index.html b/picos/Picos_thread/index.html new file mode 100644 index 00000000..d136ca34 --- /dev/null +++ b/picos/Picos_thread/index.html @@ -0,0 +1,2 @@ + +Picos_thread (picos.Picos_thread)

Module Picos_thread

Minimalistic thread API available with or without threads.posix.

val is_main_thread : unit -> bool

is_main_thread () determines whether running on the main thread of the application.

module TLS : sig ... end

Thread-local storage.

diff --git a/picos/_doc-dir/CHANGES.md b/picos/_doc-dir/CHANGES.md new file mode 100644 index 00000000..49f04748 --- /dev/null +++ b/picos/_doc-dir/CHANGES.md @@ -0,0 +1,171 @@ +## 0.5.0 + +- Major additions, changes, bug fixes, improvements, and restructuring + (@polytypic, @c-cube) + + - Additions: + + - Minimalistic Cohttp implementation + - Implicitly propagated `Flock` of fibers for structured concurrency + - Option to terminate `Bundle` and `Flock` on return + - `Event` abstraction + - Synchronization and communication primitives: + - Incremental variable or `Ivar` + - Countdown `Latch` + - `Semaphore` + - `Stream` of events + - Multi-producer, multi-consumer lock-free queue optimized for schedulers + - Multithreaded (work-stealing) FIFO scheduler + - Support `quota` for FIFO based schedulers + - Transactional interface for atomically completing multiple `Computation`s + + - Changes: + + - Redesigned resource management based on `('r -> 'a) -> 'a` functions + - Redesigned `spawn` interface allowing `FLS` entries to be populated before + spawn + - Introduced concept of fatal errors, which must terminate the scheduler or + the whole program + - Simplified `FLS` interface + - Removed `Exn_bt` + + - Improvements: + + - Signficantly reduced per fiber memory usage of various sample schedulers + + - Picos has now been split into multiple packages and libraries: + + - pkg: `picos` + - lib: `picos` + - lib: `picos.domain` + - lib: `picos.thread` + - pkg: `picos_aux` + - lib: `picos_aux.htbl` + - lib: `picos_aux.mpmcq` + - lib: `picos_aux.mpscq` + - lib: `picos_aux.rc` + - pkg: `picos_lwt` + - lib: `picos_lwt` + - lib: `picos_lwt.unix` + - pkg: `picos_meta` (integration tests) + - pkg: `picos_mux` + - lib: `picos_mux.fifo` + - lib: `picos_mux.multififo` + - lib: `picos_mux.random` + - lib: `picos_mux.thread` + - pkg: `picos_std` + - lib: `picos_std.event` + - lib: `picos_std.finally` + - lib: `picos_std.structured` + - lib: `picos_std.sync` + - pkg: `picos_io` + - lib: `picos_io` + - lib: `picos_io.fd` + - lib: `picos_io.select` + - pkg: `picos_io_cohttp` + - lib: `picos_io_cohttp` + +## 0.4.0 + +- Renamed `Picos_mpsc_queue` to `Picos_mpscq`. (@polytypic) + +- Core API changes: + + - Added `Computation.returned`. (@polytypic) + +- `Lwt` interop improvements: + + - Fixed `Picos_lwt` handling of `Cancel_after` to not raise in case of + cancelation. (@polytypic) + + - Redesigned `Picos_lwt` to take a `System` module, which must implement a + semi thread-safe trigger mechanism to allow unblocking `Lwt` promises on the + main thread. (@polytypic) + + - Added `Picos_lwt_unix` interface to `Lwt`, which includes an internal + `System` module implemented using `Lwt_unix`. (@polytypic) + + - Dropped thunking from `Picos_lwt.await`. (@polytypic) + +- Added a randomized multicore scheduler `Picos_randos` for testing. + (@polytypic) + +- Changed `Picos_select.check_configured` to always (re)configure signal + handling on the current thread. (@polytypic) + +- `Picos_structured`: + + - Added a minimalistic `Promise` abstraction. (@polytypic) + - Changed to more consistently not treat `Terminate` as an error. (@polytypic) + +- Changed schedulers to take `~forbid` as an optional argument. (@polytypic) + +- Various minor additions, fixes, and documentation improvements. (@polytypic) + +## 0.3.0 + +- Core API changes: + + - Added `Fiber.set_computation`, which represents a semantic change + - Renamed `Fiber.computation` to `Fiber.get_computation` + - Added `Computation.attach_canceler` + - Added `Fiber.sleep` + - Added `Fiber.create_packed` + - Removed `Fiber.try_attach` + - Removed `Fiber.detach` + + Most of the above changes were motivated by work on and requirements of the + added structured concurrency library (@polytypic) + +- Added a basic user level structured concurrent programming library + `Picos_structured` (@polytypic) + +- Added a functorized `Picos_lwt` providing direct style effects based interface + to programming with Lwt (@polytypic) + +- Added missing `Picos_stdio.Unix.select` (@polytypic) + +## 0.2.0 + +- Documentation fixes and restructuring (@polytypic) +- Scheduler friendly `waitpid`, `wait`, and `system` in `Picos_stdio.Unix` for + platforms other than Windows (@polytypic) +- Added `Picos_select.configure` to allow, and sometimes require, configuring + `Picos_select` for co-operation with libraries that also deal with signals + (@polytypic) +- Moved `Picos_tls` into `Picos_thread.TLS` (@polytypic) +- Enhanced `sleep` and `sleepf` in `Picos_stdio.Unix` to block in a scheduler + friendly manner (@polytypic) + +## 0.1.0 + +- First experimental release of Picos. + + Core: + + - `picos` — A framework for interoperable effects based concurrency. + + Sample schedulers: + + - `picos.fifos` — Basic single-threaded effects based Picos compatible + scheduler for OCaml 5. + - `picos.threaded` — Basic `Thread` based Picos compatible scheduler for + OCaml 4. + + Scheduler agnostic libraries: + + - `picos.sync` — Basic communication and synchronization primitives for Picos. + - `picos.stdio` — Basic IO facilities based on OCaml standard libraries for + Picos. + - `picos.select` — Basic `Unix.select` based IO event loop for Picos. + + Auxiliary libraries: + + - `picos.domain` — Minimalistic domain API available both on OCaml 5 and on + OCaml 4. + - `picos.exn_bt` — Wrapper for exceptions with backtraces. + - `picos.fd` — Externally reference counted file descriptors. + - `picos.htbl` — Lock-free hash table. + - `picos.mpsc_queue` — Multi-producer, single-consumer queue. + - `picos.rc` — External reference counting tables for disposable resources. + - `picos.tls` — Thread-local storage. diff --git a/picos/_doc-dir/LICENSE.md b/picos/_doc-dir/LICENSE.md new file mode 100644 index 00000000..5da69623 --- /dev/null +++ b/picos/_doc-dir/LICENSE.md @@ -0,0 +1,13 @@ +Copyright © 2023 Vesa Karvonen + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/picos/_doc-dir/README.md b/picos/_doc-dir/README.md new file mode 100644 index 00000000..26e31b8e --- /dev/null +++ b/picos/_doc-dir/README.md @@ -0,0 +1,791 @@ +[API reference](https://ocaml-multicore.github.io/picos/doc/index.html) · +[Benchmarks](https://bench.ci.dev/ocaml-multicore/picos/branch/main?worker=pascal&image=bench.Dockerfile) +· +[Stdlib Benchmarks](https://bench.ci.dev/ocaml-multicore/multicore-bench/branch/main?worker=pascal&image=bench.Dockerfile) + +# **Picos** — Interoperable effects based concurrency + +Picos is a +[systems programming](https://en.wikipedia.org/wiki/Systems_programming) +interface between effects based schedulers and concurrent abstractions. + +

+ +Picos is designed to enable an _open ecosystem_ of +[interoperable](https://en.wikipedia.org/wiki/Interoperability) and +interchangeable elements of effects based cooperative concurrent programming +models such as + +- [schedulers]() that + multiplex large numbers of + [user level fibers](https://en.wikipedia.org/wiki/Green_thread) to run on a + small number of system level threads, +- mechanisms for managing fibers and for + [structuring concurrency](https://en.wikipedia.org/wiki/Structured_concurrency), +- communication and synchronization primitives, such as + [mutexes and condition variables](), + message queues, + [STM](https://en.wikipedia.org/wiki/Software_transactional_memory)s, and more, + and +- integrations with low level + [asynchronous IO](https://en.wikipedia.org/wiki/Asynchronous_I/O) systems + +by decoupling such elements from each other. + +Picos comes with a +[reference manual](https://ocaml-multicore.github.io/picos/doc/index.html) and +many sample libraries. + +⚠️ Please note that Picos is still considered experimental and unstable. + +## Introduction + +Picos addresses the incompatibility of effects based schedulers at a fundamental +level by introducing +[an _interface_ to decouple schedulers and other concurrent abstractions](https://ocaml-multicore.github.io/picos/doc/picos/Picos/index.html) +that need services from a scheduler. + +The +[core abstractions of Picos](https://ocaml-multicore.github.io/picos/doc/picos/Picos/index.html#the-architecture-of-picos) +are + +- [`Trigger`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Trigger/index.html) + — the ability to await for a signal, +- [`Computation`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Computation/index.html) + — a cancelable computation, and +- [`Fiber`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Fiber/index.html) + — an independent thread of execution, + +that are implemented partially by the Picos interface in terms of the effects + +- [`Trigger.Await`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Trigger/index.html#extension-Await) + — to suspend and resume a fiber, +- [`Computation.Cancel_after`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Computation/index.html#extension-Cancel_after) + — to cancel a computation after given period of time, +- [`Fiber.Current`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Fiber/index.html#extension-Current) + — to obtain the current fiber, +- [`Fiber.Yield`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Fiber/index.html#extension-Yield) + — to request rescheduling, and +- [`Fiber.Spawn`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Fiber/index.html#extension-Spawn) + — to start a new fiber. + +The partial implementation of the abstractions and the effects define a contract +between schedulers and other concurrent abstractions. By handling the Picos +effects according to the contract a scheduler becomes _Picos compatible_, which +allows any abstractions written against the Picos interface, i.e. _Implemented +in Picos_, to be used with the scheduler. + +### Understanding cancelation + +A central idea or goal of Picos is to provide a collection of building blocks +for parallelism safe cancelation that allows the implementation of both blocking +abstractions as well as the implementation of abstractions for structuring +fibers for cancelation or managing the propagation and scope of cancelation. + +While cancelation, which is essentially a kind of asynchronous exception or +signal, is not necessarily recommended as a general control mechanism, the +ability to cancel fibers in case of errors is crucial for the implementation of +practical concurrent programming models. + +Consider the following characteristic +[example](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_structured/index.html#understanding-cancelation): + +```ocaml skip +Mutex.protect mutex begin fun () -> + while true do + Condition.wait condition mutex + done +end +``` + +Assume that a fiber executing the above code might be canceled, at any point, by +another fiber running in parallel. This could be necessary, for example, due to +an error that requires the application to be shut down. How could that be done +while ensuring both +[safety and liveness](https://en.wikipedia.org/wiki/Safety_and_liveness_properties)? + +- For safety, cancelation should not leave the program in an invalid state or + cause the program to leak memory. In this case, `Condition.wait` must exit + with the mutex locked, even in case of cancelation, and, as `Mutex.protect` + exits, the ownership of the mutex must be transferred to the next fiber, if + any, waiting in queue for the mutex. No references to unused objects may be + left in the mutex or the condition variable. + +- For liveness, cancelation should ensure that the fiber will eventually + continue after cancelation. In this case, cancelation could be triggered + during the `Mutex.lock` operation inside `Mutex.protect` or the + `Condition.wait` operation, when the fiber might be in a suspended state, and + cancelation should then allow the fiber to continue. + +The set of abstractions, `Trigger`, `Computation`, and `Fiber`, work together +[to support cancelation](https://ocaml-multicore.github.io/picos/doc/picos/Picos/index.html#cancelation-in-picos). +Briefly, a fiber corresponds to an independent thread of execution and every +fiber is associated with a computation at all times. When a fiber creates a +trigger in order to await for a signal, it ask the scheduler to suspend the +fiber on the trigger. Assuming the fiber has not forbidden the propagation of +cancelation, which is required, for example, in the implementation of +`Condition.wait` to lock the mutex upon exit, the scheduler must also attach the +trigger to the computation associated with the fiber. If the computation is then +canceled before the trigger is otherwise signaled, the trigger will be signaled +by the cancelation of the computation, and the fiber will be resumed by the +scheduler as canceled. + +This cancelable suspension protocol and its partial implementation designed +around the first-order +[`Trigger.Await`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Trigger/index.html#extension-Await) +effect creates a clear separation between schedulers and user code running in +fibers and is designed to handle the possibility of a trigger being signaled or +a computation being canceled at any point during the suspension of a fiber. +Schedulers are given maximal freedom to decide which fiber to resume next. As an +example, a scheduler could give priority to canceled fibers — going as far +as moving a fiber already in the ready queue of the scheduler to the front of +the queue at the point of cancelation — based on the assumption that user +code promptly cancels external requests and frees critical resources. + +### `Trigger` + +A trigger provides the ability to await for a signal and is perhaps the best +established and least controversial element of the Picos interface. + +Here is an extract from the signature of the +[`Trigger` module](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Trigger/index.html): + + + +```ocaml skip +type t +val create : unit -> t +val await : t -> (exn * Printexc.raw_backtrace) option +val signal : t -> unit +val on_signal : (* for schedulers *) +``` + +The idea is that a fiber may create a trigger, insert it into some shared data +structure, and then call `await` to ask the scheduler to suspend the fiber until +something signals the trigger. When `await` returns an exception with a +backtrace it means that the fiber has been canceled. + +As an example, let's consider the implementation of an `Ivar` or incremental or +single-assignment variable: + +```ocaml skip +type 'a t +val create : unit -> 'a t +val try_fill : 'a t -> 'a -> bool +val read : 'a t -> 'a +``` + +An `Ivar` is created as empty and can be filled with a value once. An attempt to +read an `Ivar` blocks until the `Ivar` is filled. + +Using `Trigger` and `Atomic`, we can represent an `Ivar` as follows: + +```ocaml +type 'a state = + | Filled of 'a + | Empty of Trigger.t list + +type 'a t = 'a state Atomic.t +``` + +The `try_fill` operation is then fairly straightforward to implement: + +```ocaml +let rec try_fill t value = + match Atomic.get t with + | Filled _ -> false + | Empty triggers as before -> + let after = Filled value in + if Atomic.compare_and_set t before after then + begin + List.iter Trigger.signal triggers; (* ! *) + true + end + else + try_fill t value +``` + +The interesting detail above is that after successfully filling an `Ivar`, the +triggers are signaled. This allows the `await` inside the `read` operation to +return: + + + +```ocaml +let rec read t = + match Atomic.get t with + | Filled value -> value + | Empty triggers as before -> + let trigger = Trigger.create () in + let after = Empty (trigger :: triggers) in + if Atomic.compare_and_set t before after then + match Trigger.await trigger with + | None -> read t + | Some (exn, bt) -> + cleanup t trigger; (* ! *) + Printexc.raise_with_backtrace exn bt + else + read t +``` + +An important detail above is that when `await` returns an exception with a +backtrace, meaning that the fiber has been canceled, the `cleanup` operation +(which is omitted) is called to remove the `trigger` from the `Ivar` to avoid +potentially accumulating unbounded numbers of triggers in an empty `Ivar`. + +As simple as it is, the design of `Trigger` is far from arbitrary: + +- First of all, `Trigger` has single-assignment semantics. After being signaled, + a trigger takes a constant amount of space and does not point to any other + heap object. This makes it easier to reason about the behavior and can also + help to avoid leaks or optimize data structures containing triggers, because + it is safe to hold bounded amounts of signaled triggers. + +- The `Trigger` abstraction is essentially first-order, which provides a clear + separation between a scheduler and programs, or fibers, running on a + scheduler. The `await` operation performs the `Await` effect, which passes the + trigger to the scheduler. The scheduler then attaches its own callback to the + trigger using `on_signal`. This way a scheduler does not call arbitrary user + specified code in the `Await` effect handler. + +- Separating the creation of a trigger from the `await` operation allows one to + easily insert a trigger into any number of places and allows the trigger to be + potentially concurrently signaled before the `Await` effect is performed in + which case the effect can be skipped entirely. + +- No value is propagated with a trigger. This makes triggers simpler and makes + it less likely for one to e.g. accidentally drop such a value. In many cases, + like with the `Ivar`, there is already a data structure through which values + can be propagated. + +- The `signal` operation gives no indication of whether a fiber will then be + resumed as canceled or not. This gives maximal flexibility for the scheduler + and also makes it clear that cancelation must be handled based on the return + value of `await`. + +### `Computation` + +A `Computation` basically holds the status, i.e. _running_, _returned_, or +_canceled_, of some sort of computation and allows anyone with access to the +computation to attach triggers to it to be signaled in case the computation +stops running. + +Here is an extract from the signature of the +[`Computation` module](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Computation/index.html): + +```ocaml skip +type 'a t + +val create : unit -> 'a t + +val try_attach : 'a t -> Trigger.t -> bool +val detach : 'a t -> Trigger.t -> unit + +val try_return : 'a t -> 'a -> bool +val try_cancel : 'a t -> exn -> Printexc.raw_backtrace -> bool + +val check : 'a t -> unit +val await : 'a t -> 'a +``` + +A `Computation` directly provides a superset of the functionality of the `Ivar` +we sketched in the previous section: + +```ocaml +type 'a t = 'a Computation.t +let create : unit -> 'a t = Computation.create +let try_fill : 'a t -> 'a -> bool = + Computation.try_return +let read : 'a t -> 'a = Computation.await +``` + +However, what really makes the `Computation` useful is the ability to +momentarily attach triggers to it. A `Computation` essentially implements a +specialized lock-free bag of triggers, which allows one to implement dynamic +completion propagation networks. + +The `Computation` abstraction is also designed with both simplicity and +flexibility in mind: + +- Similarly to `Trigger`, `Computation` has single-assignment semantics, which + makes it easier to reason about. + +- Unlike a typical cancelation context of a structured concurrency model, + `Computation` is unopinionated in that it does not impose a specific + hierarchical structure. + +- Anyone may ask to be notified when a `Computation` is completed by attaching + triggers to it and anyone may complete a `Computation`. This makes + `Computation` an omnidirectional communication primitive. + +Interestingly, and unintentionally, it turns out that, given +[the ability to complete two (or more) computations atomically](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Computation/Tx/index.html), +`Computation` is essentially expressive enough to implement the +[event](https://ocaml.org/manual/latest/api/Event.html) abstraction of +[Concurrent ML](https://en.wikipedia.org/wiki/Concurrent_ML). The same features +that make `Computation` suitable for implementing more or less arbitrary dynamic +completion propagation networks make it suitable for implementing Concurrent ML +style abstractions. + +### `Fiber` + +A fiber corresponds to an independent thread of execution. Technically an +effects based scheduler creates a fiber, effectively giving it an identity, as +it runs some function under its handler. The `Fiber` abstraction provides a way +to share a proxy identity, and a bit of state, between a scheduler and other +concurrent abstractions. + +Here is an extract from the signature of the +[`Fiber` module](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Fiber/index.html): + +```ocaml skip +type t + +val current : unit -> t + +val create : forbid:bool -> 'a Computation.t -> t +val spawn : t -> (t -> unit) -> unit + +val get_computation : t -> Computation.packed +val set_computation : t -> Computation.packed -> unit + +val has_forbidden : t -> bool +val exchange : t -> forbid:bool -> bool + +module FLS : sig (* ... *) end +``` + +Fibers are where all of the low level bits and pieces of Picos come together, +which makes it difficult to give both meaningful and concise examples, but let's +implement a slightly simplistic structured concurrency mechanism: + +```ocaml skip +type t (* represents a scope *) +val run : (t -> unit) -> unit +val fork : t -> (unit -> unit) -> unit +``` + +The idea here is that `run` creates a "scope" and waits until all of the fibers +forked into the scope have finished. In case any fiber raises an unhandled +exception, or the main fiber that created the scope is canceled, all of the +fibers are canceled and an exception is raised. To keep things slightly simpler, +only the first exception is kept. + +A scope can be represented by a simple record type: + +```ocaml +type t = { + count : int Atomic.t; + inner : unit Computation.t; + ended : Trigger.t; +} +``` + +The idea is that after a fiber is finished, we decrement the count and if it +becomes zero, we finish the computation and signal the main fiber that the scope +has ended: + +```ocaml +let decr t = + let n = Atomic.fetch_and_add t.count (-1) in + if n = 1 then begin + Computation.finish t.inner; + Trigger.signal t.ended + end +``` + +When forking a fiber, we increment the count unless it already was zero, in +which case we raise an error: + +```ocaml +let rec incr t = + let n = Atomic.get t.count in + if n = 0 then invalid_arg "ended"; + if not (Atomic.compare_and_set t.count n (n + 1)) + then incr t +``` + +The fork operation is now relatively straightforward to implement: + +```ocaml +let fork t action = + incr t; + try + let main _ = + match action () with + | () -> decr t + | exception exn -> + let bt = Printexc.get_raw_backtrace () in + Computation.cancel t.inner exn bt; + decr t + in + let fiber = + Fiber.create ~forbid:false t.inner + in + Fiber.spawn fiber main + with canceled_exn -> + decr t; + raise canceled_exn +``` + +The above `fork` first increments the count and then tries to spawn a fiber. The +Picos interface specifies that when `Fiber.spawn` returns normally, the action, +`main`, must be called by the scheduler. This allows us to ensure that the +increment is always matched with a decrement. + +Setting up a scope is the most complex operation: + + + +```ocaml +let run body = + let count = Atomic.make 1 in + let inner = Computation.create () in + let ended = Trigger.create () in + let t = { count; inner; ended } in + let fiber = Fiber.current () in + let (Packed outer) = + Fiber.get_computation fiber + in + let canceler = + Computation.attach_canceler + ~from:outer + ~into:t.inner + in + match + Fiber.set_computation fiber (Packed t.inner); + body t + with + | () -> join t outer canceler fiber + | exception exn -> + let bt = Printexc.get_raw_backtrace () in + Computation.cancel t.inner exn bt; + join t outer canceler fiber; + Printexc.raise_with_backtrace exn bt +``` + +The `Computation.attach_canceler` operation attaches a special trigger to +propagate cancelation from one computation into another. After the body exits, +`join` + +```ocaml +let join t outer canceler fiber = + decr t; + Fiber.set_computation fiber (Packed outer); + let forbid = Fiber.exchange fiber ~forbid:true in + Trigger.await t.ended |> ignore; + Fiber.set fiber ~forbid; + Computation.detach outer canceler; + Computation.check t.inner; + Fiber.check fiber +``` + +is called to wait for the scoped fibers and restore the state of the main fiber. +An important detail is that propagation of cancelation is forbidden by setting +the `forbid` flag to `true` before the call of `Trigger.await`. This is +necessary to ensure that `join` does not exit, due to the fiber being canceled, +before all of the child fibers have actually finished. Finally, `join` checks +the inner computation and the fiber, which means that an exception will be +raised in case either was canceled. + +The design of `Fiber` includes several key features: + +- The low level design allows one to both avoid unnecessary overheads, such as + allocating a `Computation.t` for every fiber, when implementing simple + abstractions and also to implement more complex behaviors that might prove + difficult given e.g. a higher level design with a built-in notion of + hierarchy. + +- As `Fiber.t` stores the `forbid` flag and the `Computation.t` associated with + the fiber one need not pass those as arguments through the program. This + allows various concurrent abstractions to be given traditional interfaces, + which would otherwise need to be complicated. + +- Effects are relatively expensive. The cost of performing effects can be + amortized by obtaining the `Fiber.t` once and then manipulating it multiple + times. + +- A `Fiber.t` also provides an identity for the fiber. It has so far proven to + be sufficient for most purposes. Fiber local storage, which we do not cover + here, can be used to implement, for example, a unique integer id for fibers. + +### Assumptions + +Now, consider the `Ivar` abstraction presented earlier as an example of the use +of the `Trigger` abstraction. That `Ivar` implementation, as well as the +`Computation` based implementation, works exactly as desired inside the scope +abstraction presented in the previous section. In particular, a blocked +`Ivar.read` can be canceled, either when another fiber in a scope raises an +unhandled exception or when the main fiber of the scope is canceled, which +allows the fiber to continue by raising an exception after cleaning up. In fact, +Picos comes with a number of libraries that all would work quite nicely with the +examples presented here. + +For example, a library provides an operation to run a block with a timeout on +the current fiber. One could use it with `Ivar.read` to implement a read +operation +[with a timeout](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_structured/Control/index.html#val-terminate_after): + +```ocaml +let read_in ~seconds ivar = + Control.terminate_after ~seconds @@ fun () -> + Ivar.read ivar +``` + +This interoperability is not accidental. For example, the scope abstraction +basically assumes that one does not use `Fiber.set_computation`, in an arbitrary +unscoped manner inside the scoped fibers. An idea with the Picos interface +actually is that it is not supposed to be used by applications at all and most +higher level libraries should be built on top of libraries that do not directly +expose elements of the Picos interface. + +Perhaps more interestingly, there are obviously limits to what can be achieved +in an "interoperable" manner. Imagine an operation like + +```ocaml skip +val at_exit : (unit -> unit) -> unit +``` + +that would allow one to run an action just before a fiber exits. One could, of +course, use a custom spawn function that would support such cleanup, but then +`at_exit` could only be used on fibers spawned through that particular spawn +function. + +### The effects + +As mentioned previously, the Picos interface is implemented partially in terms +of five effects: + +```ocaml version>=5.0.0 +type _ Effect.t += + | Await : Trigger.t -> (exn * Printexc.raw_backtrace) option Effect.t + | Cancel_after : { + seconds : float; + exn: exn; + bt : Printexc.raw_backtrace; + computation : 'a Computation.t; + } + -> unit Effect.t + | Current : t Effect.t + | Yield : unit Effect.t + | Spawn : { + fiber : Fiber.t; + main : (Fiber.t -> unit); + } + -> unit Effect.t +``` + +A scheduler must handle those effects as specified in the Picos documentation. + +The Picos interface does not, in particular, dictate which ready fibers a +scheduler must run next and on which domains. Picos also does not require that a +fiber should stay on the domain on which it was spawned. Abstractions +implemented against the Picos interface should not assume any particular +scheduling. + +Picos actually comes with +[a randomized multithreaded scheduler](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_randos/index.html), +that, after handling any of the effects, picks the next ready fiber randomly. It +has proven to be useful for testing that abstractions implemented in Picos do +not make invalid scheduling assumptions. + +When a concurrent abstraction requires a particular scheduling, it should +primarily be achieved through the use of synchronization abstractions like when +programming with traditional threads. Application programs may, of course, pick +specific schedulers. + +## Status and results + +We have an experimental design and implementation of the core Picos interface as +illustrated in the previous section. We have also created several _Picos +compatible_ +[sample schedulers](https://ocaml-multicore.github.io/picos/doc/picos_mux/index.html). +A scheduler, in this context, just multiplexes fibers to run on one or more +system level threads. We have also created some sample higher-level +[scheduler agnostic libraries](https://ocaml-multicore.github.io/picos/doc/picos_std/index.html) +_Implemented in Picos_. These libraries include +[a library for resource management](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_finally/index.html), +[a library for structured concurrency](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_structured/index.html), +[a library of synchronization primitives](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_sync/index.html), +and +[an asynchronous I/O library](https://ocaml-multicore.github.io/picos/doc/picos_io/Picos_io/index.html). +The synchronization library and the I/O library intentionally mimic libraries +that come with the OCaml distribution. All of the libraries work with all of the +schedulers and all of these _elements_ are interoperable and entirely opt-in. + +What is worth explicitly noting is that all of these schedulers and libraries +are small, independent, and highly modular pieces of code. They all crucially +depend on and are decoupled from each other via the core Picos interface +library. A basic single threaded scheduler implementation requires only about +100 lines of code (LOC). A more complex parallel scheduler might require a +couple of hundred LOC. The scheduler agnostic libraries are similarly small. + +Here is an +[example](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_structured/index.html#a-simple-echo-server-and-clients) +of a concurrent echo server using the scheduler agnostic libraries provided as +samples: + +```ocaml +let run_server server_fd = + Unix.listen server_fd 8; + Flock.join_after begin fun () -> + while true do + let@ client_fd = instantiate Unix.close @@ fun () -> + Unix.accept ~cloexec:true server_fd |> fst + in + Flock.fork begin fun () -> + let@ client_fd = move client_fd in + Unix.set_nonblock client_fd; + let bs = Bytes.create 100 in + let n = + Unix.read client_fd bs 0 (Bytes.length bs) + in + Unix.write client_fd bs 0 n |> ignore + end + done + end +``` + +The +[`Unix`](https://ocaml-multicore.github.io/picos/doc/picos_io/Picos_io/Unix/index.html) +module is provided by the I/O library. The operations on file descriptors on +that module, such as `accept`, `read`, and `write`, use the Picos interface to +suspend fibers allowing other fibers to run while waiting for I/O. The +[`Flock`](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_structured/Flock/index.html) +module comes from the structured concurrency library. A call of +[`join_after`](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_structured/Flock/index.html#val-join_after) +returns only after all the fibers +[`fork`](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_structured/Flock/index.html#val-fork)ed +into the flock have terminated. If the main fiber of the flock is canceled, or +any fiber within the flock raises an unhandled exception, all the fibers within +the flock will be canceled and an exception will be raised on the main fiber of +the flock. The +[`let@`](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_finally/index.html#val-let@), +[`finally`](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_finally/index.html#val-instantiate), +and +[`move`](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_finally/index.html#val-move) +operations come from the resource management library and allow dealing with +resources in a leak-free manner. The responsibility to close the `client_fd` +socket is +[`move`](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_finally/index.html#val-move)d +from the main server fiber to a fiber forked to handle that client. + +We should emphasize that the above is just an example. The Picos interface +should be both expressive and efficient enough to support practical +implementations of many different kinds of concurrent programming models. Also, +as described previously, the Picos interface does not, for example, internally +implement structured concurrency. However, the abstractions provided by Picos +are designed to allow structured and unstructured concurrency to be _Implemented +in Picos_ as libraries that will then work with any _Picos compatible_ scheduler +and with other concurrent abstractions. + +Finally, an interesting demonstration that Picos really fundamentally is an +interface is +[a prototype _Picos compatible_ direct style interface to Lwt](https://ocaml-multicore.github.io/picos/doc/picos_lwt/Picos_lwt/index.html). +The implementation uses shallow effect handlers and defers all scheduling +decisions to Lwt. Running a program with the scheduler returns a Lwt promise. + +## Future work + +As mentioned previously, Picos is still an ongoing project and the design is +considered experimental. We hope that Picos soon matures to serve the needs of +both the commercial users of OCaml and the community at large. + +Previous sections already touched a couple of updates currently in development, +such as the support for finalizing resources stored in +[`FLS`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Fiber/FLS/index.html) +and the development of Concurrent ML style abstractions. We also have ongoing +work to formalize aspects of the Picos interface. + +One potential change we will be investigating is whether the +[`Computation`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Computation/index.html) +abstraction should be simplified to only support cancelation. + +The implementation of some operations, such as +[`Fiber.current`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Fiber/index.html#val-current) +to retrieve the current fiber proxy identity, do not strictly need to be +effects. Performing an effect is relatively expensive and we will likely design +a mechanism to store a reference to the current fiber in some sort of local +storage, which could significantly improve the performance of certain +abstractions, such as checked mutexes, that need to access the current fiber. + +We also plan to develop a minimalist library for spawning threads over domains, +much like Moonpool, in a cooperative manner for schedulers and other libraries. + +We also plan to make Domainslib Picos compatible, which will require developing +a more efficient non-effects based interface for spawning fibers, and +investigate making Eio Picos compatible. + +We also plan to design and implement asynchronous IO libraries for Picos using +various system call interface for asynchronous IO such as io_uring. + +Finally, Picos is supposed to be an _open ecosystem_. If you have feedback or +would like to work on something mentioned above, let us know. + +## Motivation + +There are already several concrete effects-based concurrent programming +libraries and models being developed. Here is a list of some such publicly +available projects:[\*](https://xkcd.com/927/) + +1. [Affect](https://github.com/dbuenzli/affect) — "Composable concurrency + primitives with OCaml effects handlers (unreleased)", +2. [Domainslib](https://github.com/ocaml-multicore/domainslib) — + "Nested-parallel programming", +3. [Eio](https://github.com/ocaml-multicore/eio) — "Effects-Based Parallel IO + for OCaml", +4. [Fuseau](https://github.com/c-cube/fuseau) — "Lightweight fiber library for + OCaml 5", +5. [Miou](https://github.com/robur-coop/miou) — "A simple scheduler for OCaml + 5", +6. [Moonpool](https://github.com/c-cube/moonpool) — "Commodity thread pools for + OCaml 5", and +7. [Riot](https://github.com/leostera/riot) — "An actor-model multi-core + scheduler for OCaml 5". + +All of the above libraries are mutually incompatible with each other with the +exception that Domainslib, Eio, and Moonpool implement an earlier +interoperability proposal called +[domain-local-await](https://github.com/ocaml-multicore/domain-local-await/) or +DLA, which allows a concurrent programming library like +[Kcas](https://github.com/ocaml-multicore/kcas/)[\*](https://github.com/ocaml-multicore/kcas/pull/136) +to work on all of those. Unfortunately, DLA, by itself, is known to be +insufficient and the design has not been universally accepted. + +By introducing a scheduler interface and key libraries, such as an IO library, +implemented on top of the interface, we hope that the scarce resources of the +OCaml community are not further divided into mutually incompatible ecosystems +built on top of such mutually incompatible concurrent programming libraries, +while, simultaneously, making it possible to experiment with many kinds of +concurrent programming models. + +It should be +technically[\*](https://www.youtube.com/watch?v=hou0lU8WMgo) possible +for all the previously mentioned libraries, except +[Miou](https://github.com/robur-coop/miou), to + +1. be made + [Picos compatible](https://ocaml-multicore.github.io/picos/doc/picos/index.html#picos-compatible), + i.e. to handle the Picos effects, and +2. have their elements + [implemented in Picos](https://ocaml-multicore.github.io/picos/doc/picos/index.html#implemented-in-picos), + i.e. to make them usable on other Picos-compatible schedulers. + +Please read +[the reference manual](https://ocaml-multicore.github.io/picos/doc/index.html) +for further information. diff --git a/picos/_doc-dir/odoc-pages/index.mld b/picos/_doc-dir/odoc-pages/index.mld new file mode 100644 index 00000000..b06ab18a --- /dev/null +++ b/picos/_doc-dir/odoc-pages/index.mld @@ -0,0 +1,150 @@ +{0 Picos — Interoperable effects based concurrency} + +{1 Introduction} + +{!Picos} is a {{:https://en.wikipedia.org/wiki/Systems_programming} systems +programming} interface between effects based schedulers and concurrent +abstractions. Picos is designed to enable an ecosystem of +{{:https://en.wikipedia.org/wiki/Interoperability} interoperable} elements of +{{:https://v2.ocaml.org/manual/effects.html} effects based} +{{:https://en.wikipedia.org/wiki/Cooperative_multitasking} cooperative} +{{:https://en.wikipedia.org/wiki/Concurrent_computing} concurrent programming +models} such as + +- {{:https://en.wikipedia.org/wiki/Scheduling_(computing)} schedulers} that + multiplex large numbers of {{:https://en.wikipedia.org/wiki/Green_thread} user + level fibers} to run on a small number of system level threads, +- mechanisms for managing fibers and for + {{:https://en.wikipedia.org/wiki/Structured_concurrency} structuring + concurrency}, +- communication and synchronization primitives, such as + {{:https://en.wikipedia.org/wiki/Monitor_(synchronization)} mutexes and + condition variables}, message queues, + {{:https://en.wikipedia.org/wiki/Software_transactional_memory} STMs}, and + more, and +- integrations with low level {{:https://en.wikipedia.org/wiki/Asynchronous_I/O} + asynchronous IO} systems. + +If you are the author of an application level concurrent programming library or +framework, then Picos should not fundamentally be competing with your work. +However, Picos and libraries built on top of Picos probably do have overlap with +your work and making your work Picos compatible may offer benefits: + +- You may find it useful that the {{!Picos} core} of Picos provides parallelism + safe building blocks for cancelation, which is a particularly tricky problem + to get right. +- You may find it useful that you don't have to reinvent many of the basic + communication and synchronization abstractions such as mutexes and condition + variables, promises, concurrent bounded queues, channels, and what not. +- You may benefit from further non-trivial libraries, such as IO libraries, that + you don't have to reimplement. +- Potential users of your work may be reassured and benefit from the ability to + mix-and-match your work with other Picos compatible libraries and frameworks. + +Of course, interoperability does have some costs. It takes time to understand +Picos and it takes time to implement Picos compatibility. Implementing your +programming model elements in terms of the Picos interface may not always give +ideal results. To address concerns such as those, a conscious effort has been +made to keep Picos as minimal and unopinionated as possible. + +{2 Interoperability} + +Picos is essentially an interface between schedulers and concurrent +abstractions. Two phrases, {i Picos compatible} and {i Implemented in Picos}, +are used to describe the opposing sides of this contract. + +{3 Picos compatible} + +The idea is that schedulers provide their own handlers for the Picos effects. +By handling the Picos effects a scheduler allows any libraries built on top of +the Picos interface to be used with the scheduler. Such a scheduler is then +said to be {i Picos compatible}. + +{3 Implemented in Picos} + +A scheduler is just one element of a concurrent programming model. Separately +from making a scheduler Picos compatible, one may choose to implement other +elements of the programming model, e.g. a particular approach to structuring +concurrency or a particular collection of communication and synchronization +primitives, in terms of the Picos interface. Such scheduler agnostic elements +can then be used on any Picos compatible scheduler and are said to be {i +Implemented in Picos}. + +{2 Design goals and principles} + +The {{!Picos} core} of Picos is designed and developed with various goals and +principles in mind. + +- {b Simple}: Picos should be kept as simple as possible. +- {b Minimal}: Picos should be kept minimal. The dependency footprint should be + as small as possible. Convenience features should be built on top of the + interface. +- {b Safe}: Picos should be designed with safety in mind. The implementation + must be data race free. The interface should promote and always allow proper + resource management. +- {b Unopinionated}: Picos should not make strong design choices that are + controversial. +- {b Flexible}: Picos should allow higher level libraries as much freedom as + possible to make their own design choices. + +The documentation of the {{!Picos} concepts} includes design rationale for some +of the specific ideas behind their detailed design. + +{3 Constraints Liberate, Liberties Constrain} + +Picos aims to be unopinionated and flexible enough to allow higher level +libraries to provide many different kinds of concurrent programming models. +While it is impossible to give a complete list of what Picos does not dictate, +it is perhaps illuminating to explicitly mention some of those: + +- Picos does not implement + {{:https://en.wikipedia.org/wiki/Capability-based_security} capability-based + security}. Higher level libraries with or without capabilities may be built + on top of Picos. +- Picos never cancels computations implicitly. Higher level libraries may + decide when cancelation should be allowed to take effect. +- Picos does not dictate which fiber should be scheduled next after a Picos + effect. Different schedulers may freely use desired data structures (queues, + work-stealing deques, stacks, priority queues, ...) and, after handling any + Picos effect, freely decide which fiber to run next. +- Picos does not dictate how fibers should be managed. It is possible to + implement both unstructured and structured concurrent programming models on + top of Picos. +- Picos does not dictate which mechanisms applications should use for + communication and synchronization. It is possible to build many different + kinds of communication and synchronization mechanisms on top of Picos + including mutexes and condition variables, STMs, asynchronous and synchronous + message passing, {{:https://en.wikipedia.org/wiki/Actor_model} actors}, and + more. +- Picos does not dictate that there should be a connection between the scheduler + and other elements of the concurrent programming model. It is possible to + provide those separately and mix-and-match. +- Picos does not dictate which library to use for IO. It is possible to build + direct-style asynchronous IO libraries on top of Picos that can then be used + with any Picos compatible schedulers or concurrent programming models. + +Let's build an incredible ecosystem of interoperable concurrent programming +libraries and frameworks! + +{1 Libraries} + +{!modules: + Picos + Picos_domain + Picos_thread +} + +{1 Conventions} + +Many operation in the Picos libraries use +{{:https://en.wikipedia.org/wiki/Non-blocking_algorithm} non-blocking} +algorithms. Unless explicitly specified otherwise, + +- non-blocking operations in Picos are {i atomic} or {i strictly linearizable} + (i.e. {{:https://en.wikipedia.org/wiki/Linearizability} linearizable} and + {{:https://en.wikipedia.org/wiki/Database_transaction_schedule#Serializable} + serializable}), and +- {{:https://en.wikipedia.org/wiki/Non-blocking_algorithm#Lock-freedom} + lock-free} operations in Picos are designed to avoid having competing + operations of widely different complexities, which should make such operations + much less prone to starvation. diff --git a/picos/index.html b/picos/index.html new file mode 100644 index 00000000..1d9c2ba6 --- /dev/null +++ b/picos/index.html @@ -0,0 +1,2 @@ + +index (picos.index)

Package picos

Introduction

Picos is a systems programming interface between effects based schedulers and concurrent abstractions. Picos is designed to enable an ecosystem of interoperable elements of effects based cooperative concurrent programming models such as

If you are the author of an application level concurrent programming library or framework, then Picos should not fundamentally be competing with your work. However, Picos and libraries built on top of Picos probably do have overlap with your work and making your work Picos compatible may offer benefits:

  • You may find it useful that the core of Picos provides parallelism safe building blocks for cancelation, which is a particularly tricky problem to get right.
  • You may find it useful that you don't have to reinvent many of the basic communication and synchronization abstractions such as mutexes and condition variables, promises, concurrent bounded queues, channels, and what not.
  • You may benefit from further non-trivial libraries, such as IO libraries, that you don't have to reimplement.
  • Potential users of your work may be reassured and benefit from the ability to mix-and-match your work with other Picos compatible libraries and frameworks.

Of course, interoperability does have some costs. It takes time to understand Picos and it takes time to implement Picos compatibility. Implementing your programming model elements in terms of the Picos interface may not always give ideal results. To address concerns such as those, a conscious effort has been made to keep Picos as minimal and unopinionated as possible.

Interoperability

Picos is essentially an interface between schedulers and concurrent abstractions. Two phrases, Picos compatible and Implemented in Picos, are used to describe the opposing sides of this contract.

Picos compatible

The idea is that schedulers provide their own handlers for the Picos effects. By handling the Picos effects a scheduler allows any libraries built on top of the Picos interface to be used with the scheduler. Such a scheduler is then said to be Picos compatible.

Implemented in Picos

A scheduler is just one element of a concurrent programming model. Separately from making a scheduler Picos compatible, one may choose to implement other elements of the programming model, e.g. a particular approach to structuring concurrency or a particular collection of communication and synchronization primitives, in terms of the Picos interface. Such scheduler agnostic elements can then be used on any Picos compatible scheduler and are said to be Implemented in Picos.

Design goals and principles

The core of Picos is designed and developed with various goals and principles in mind.

  • Simple: Picos should be kept as simple as possible.
  • Minimal: Picos should be kept minimal. The dependency footprint should be as small as possible. Convenience features should be built on top of the interface.
  • Safe: Picos should be designed with safety in mind. The implementation must be data race free. The interface should promote and always allow proper resource management.
  • Unopinionated: Picos should not make strong design choices that are controversial.
  • Flexible: Picos should allow higher level libraries as much freedom as possible to make their own design choices.

The documentation of the concepts includes design rationale for some of the specific ideas behind their detailed design.

Constraints Liberate, Liberties Constrain

Picos aims to be unopinionated and flexible enough to allow higher level libraries to provide many different kinds of concurrent programming models. While it is impossible to give a complete list of what Picos does not dictate, it is perhaps illuminating to explicitly mention some of those:

  • Picos does not implement capability-based security. Higher level libraries with or without capabilities may be built on top of Picos.
  • Picos never cancels computations implicitly. Higher level libraries may decide when cancelation should be allowed to take effect.
  • Picos does not dictate which fiber should be scheduled next after a Picos effect. Different schedulers may freely use desired data structures (queues, work-stealing deques, stacks, priority queues, ...) and, after handling any Picos effect, freely decide which fiber to run next.
  • Picos does not dictate how fibers should be managed. It is possible to implement both unstructured and structured concurrent programming models on top of Picos.
  • Picos does not dictate which mechanisms applications should use for communication and synchronization. It is possible to build many different kinds of communication and synchronization mechanisms on top of Picos including mutexes and condition variables, STMs, asynchronous and synchronous message passing, actors, and more.
  • Picos does not dictate that there should be a connection between the scheduler and other elements of the concurrent programming model. It is possible to provide those separately and mix-and-match.
  • Picos does not dictate which library to use for IO. It is possible to build direct-style asynchronous IO libraries on top of Picos that can then be used with any Picos compatible schedulers or concurrent programming models.

Let's build an incredible ecosystem of interoperable concurrent programming libraries and frameworks!

Libraries

  • Picos A systems programming interface between effects based schedulers and concurrent abstractions.
  • Picos_domain Minimalistic domain API available both on OCaml 5 and on OCaml 4.
  • Picos_thread Minimalistic thread API available with or without threads.posix.

Conventions

Many operation in the Picos libraries use non-blocking algorithms. Unless explicitly specified otherwise,

  • non-blocking operations in Picos are atomic or strictly linearizable (i.e. linearizable and serializable), and
  • lock-free operations in Picos are designed to avoid having competing operations of widely different complexities, which should make such operations much less prone to starvation.

Package info

changes-files
license-files
readme-files
diff --git a/picos_std/Picos_std_event/Event/index.html b/picos_std/Picos_std_event/Event/index.html new file mode 100644 index 00000000..e63effbf --- /dev/null +++ b/picos_std/Picos_std_event/Event/index.html @@ -0,0 +1,2 @@ + +Event (picos_std.Picos_std_event.Event)

Module Picos_std_event.Event

First-class synchronous communication abstraction.

Events describe a thing that might happen in the future, or a concurrent offer or request that might be accepted or succeed, but is cancelable if some other event happens first.

See the Picos_io_select library for an example.

ℹ️ This module intentionally mimics the Event module provided by the OCaml POSIX threads library.

type !'a t

An event returning a value of type 'a.

type 'a event = 'a t

An alias for the Event.t type to match the Event module signature.

val always : 'a -> 'a t

always value returns an event that can always be committed to resulting in the given value.

Composing events

val choose : 'a t list -> 'a t

choose events return an event that offers all of the given events and then commits to at most one of them.

val wrap : 'b t -> ('b -> 'a) -> 'a t

wrap event fn returns an event that acts as the given event and then applies the given function to the value in case the event is committed to.

val map : ('b -> 'a) -> 'b t -> 'a t

map fn event is equivalent to wrap event fn.

val guard : (unit -> 'a t) -> 'a t

guard thunk returns an event that, when synchronized, calls the thunk, and then behaves like the resulting event.

⚠️ Raising an exception from a guard thunk will result in raising that exception out of the sync. This may result in dropping the result of an event that committed just after the exception was raised. This means that you should treat an unexpected exception raised from sync as a fatal error.

Consuming events

val sync : 'a t -> 'a

sync event synchronizes on the given event.

Synchronizing on an event executes in three phases:

  1. In the first phase offers or requests are made to communicate.
  2. One of the offers or requests is committed to and all the other offers and requests are canceled.
  3. A final result is computed from the value produced by the event.

⚠️ sync event does not wait for the canceled concurrent requests to terminate. This means that you should arrange for guaranteed cleanup through other means such as the use of structured concurrency.

val select : 'a t list -> 'a

select events is equivalent to sync (choose events).

Primitive events

ℹ️ The Computation concept of Picos can be seen as a basic single-shot atomic event. This module builds on that concept to provide a composable API to concurrent services exposed through computations.

type 'a request = {
  1. request : 'r. (unit -> 'r) Picos.Computation.t -> ('a -> 'r) -> unit;
}

Represents a function that requests a concurrent service to update a computation.

ℹ️ The computation passed to a request may be completed by some other event at any point. All primitive requests should be implemented carefully to take that into account. If the computation is completed by some other event, then the request should be considered as canceled, take no effect, and not leak any resources.

⚠️ Raising an exception from a request function will result in raising that exception out of the sync. This may result in dropping the result of an event that committed just after the exception was raised. This means that you should treat an unexpected exception raised from sync as a fatal error. In addition, you should arrange for concurrent services to report unexpected errors independently of the computation being passed to the service.

val from_request : 'a request -> 'a t

from_request { request } creates an event from the request function.

val from_computation : 'a Picos.Computation.t -> 'a t

from_computation source creates an event that can be committed to once the given source computation has completed.

ℹ️ Committing to some other event does not cancel the source computation.

diff --git a/picos_std/Picos_std_event/index.html b/picos_std/Picos_std_event/index.html new file mode 100644 index 00000000..2cd04503 --- /dev/null +++ b/picos_std/Picos_std_event/index.html @@ -0,0 +1,2 @@ + +Picos_std_event (picos_std.Picos_std_event)

Module Picos_std_event

Basic event abstraction for Picos.

module Event : sig ... end

First-class synchronous communication abstraction.

diff --git a/picos_std/Picos_std_event__/index.html b/picos_std/Picos_std_event__/index.html new file mode 100644 index 00000000..9d5a4a40 --- /dev/null +++ b/picos_std/Picos_std_event__/index.html @@ -0,0 +1,2 @@ + +Picos_std_event__ (picos_std.Picos_std_event__)

Module Picos_std_event__

This module is hidden.

diff --git a/picos_std/Picos_std_event__Event/index.html b/picos_std/Picos_std_event__Event/index.html new file mode 100644 index 00000000..ac3ba24d --- /dev/null +++ b/picos_std/Picos_std_event__Event/index.html @@ -0,0 +1,2 @@ + +Picos_std_event__Event (picos_std.Picos_std_event__Event)

Module Picos_std_event__Event

This module is hidden.

diff --git a/picos_std/Picos_std_finally/index.html b/picos_std/Picos_std_finally/index.html new file mode 100644 index 00000000..7c01a3d4 --- /dev/null +++ b/picos_std/Picos_std_finally/index.html @@ -0,0 +1,71 @@ + +Picos_std_finally (picos_std.Picos_std_finally)

Module Picos_std_finally

Syntax for avoiding resource leaks for Picos.

A resource is something that is acquired and must be released after it is no longer needed.

We open both this library and a few other libraries

open Picos_io
+open Picos_std_finally
+open Picos_std_structured
+open Picos_std_sync

for the examples.

API

Basics

val let@ : ('a -> 'b) -> 'a -> 'b

let@ resource = template in scope is equivalent to template (fun resource -> scope).

ℹ️ You can use this binding operator with any template function that has a type of the form ('r -> 'a) -> 'a.

val finally : ('r -> unit) -> (unit -> 'r) -> ('r -> 'a) -> 'a

finally release acquire scope calls acquire () to obtain a resource, calls scope resource, and then calls release resource after the scope exits.

Instances

type 'r instance

Either contains a resource or is empty as the resource has been transferred, dropped, or has been borrowed temporarily.

val instantiate : ('r -> unit) -> (unit -> 'r) -> ('r instance -> 'a) -> 'a

instantiate release acquire scope calls acquire () to obtain a resource and stores it as an instance, calls scope instance. Then, if scope returns normally, awaits until the instance becomes empty. In case scope raises an exception or the fiber is canceled, the instance will be dropped.

val drop : 'r instance -> unit

drop instance releases the resource, if any, contained by the instance.

  • raises Invalid_argument

    if the resource has been borrowed and hasn't yet been returned.

val borrow : 'r instance -> ('r -> 'a) -> 'a

borrow instance scope borrows the resource stored in the instance, calls scope resource, and then returns the resource to the instance after scope exits.

  • raises Invalid_argument

    if the resource has already been borrowed and hasn't yet been returned, has already been dropped, or has already been transferred.

val transfer : 'r instance -> ('r instance -> 'a) -> 'a

transfer source transfers the resource stored in the source instance into a new target instance, calls scope target. Then, if scope returns normally, awaits until the target instance becomes empty. In case scope raises an exception or the fiber is canceled, the target instance will be dropped.

  • raises Invalid_argument

    if the resource has been borrowed and hasn't yet been returned, has already been transferred, or has been dropped unless the current fiber has been canceled, in which case the exception that the fiber was canceled with will be raised.

val move : 'r instance -> ('r -> 'a) -> 'a

move instance scope is equivalent to transfer instance (fun instance -> borrow instance scope).

Examples

Recursive server

Here is a sketch of a server that recursively forks a fiber to accept and handle a client:

let recursive_server server_fd =
+  Flock.join_after @@ fun () ->
+
+  (* recursive server *)
+  let rec accept () =
+    let@ client_fd =
+      finally Unix.close @@ fun () ->
+      Unix.accept ~cloexec:true server_fd
+      |> fst
+    in
+
+    (* fork to accept other clients *)
+    Flock.fork accept;
+
+    (* handle this client... omitted *)
+    ()
+  in
+  Flock.fork accept

Looping server

There is also a way to move instantiated resources to allow forking fibers to handle clients without leaks.

Here is a sketch of a server that accepts in a loop and forks fibers to handle clients:

let looping_server server_fd =
+  Flock.join_after @@ fun () ->
+
+  (* loop to accept clients *)
+  while true do
+    let@ client_fd =
+      instantiate Unix.close @@ fun () ->
+      Unix.accept ~cloexec:true server_fd
+      |> fst
+    in
+
+    (* fork to handle this client *)
+    Flock.fork @@ fun () ->
+      let@ client_fd = move client_fd in
+
+      (* handle client... omitted *)
+      ()
+  done

Move resource from child to parent

You can move an instantiated resource between any two fibers and borrow it before moving it. For example, you can create a resource in a child fiber, use it there, and then move it to the parent fiber:

let move_from_child_to_parent () =
+  Flock.join_after @@ fun () ->
+
+  (* for communicating a resource *)
+  let shared_ivar = Ivar.create () in
+
+  (* fork a child that creates a resource *)
+  Flock.fork begin fun () ->
+    let pretend_release () = ()
+    and pretend_acquire () = () in
+
+    (* allocate a resource *)
+    let@ instance =
+      instantiate pretend_release pretend_acquire
+    in
+
+    begin
+      (* borrow the resource *)
+      let@ resource = borrow instance in
+
+      (* use the resource... omitted *)
+      ()
+    end;
+
+    (* send the resource to the parent *)
+    Ivar.fill shared_ivar instance
+  end;
+
+  (* await for a resource from the child and own it *)
+  let@ resource = Ivar.read shared_ivar |> move in
+
+  (* use the resource... omitted *)
+  ()

The above uses an Ivar to communicate the movable resource from the child fiber to the parent fiber. Any concurrency safe mechanism could be used.

diff --git a/picos_std/Picos_std_structured/Bundle/index.html b/picos_std/Picos_std_structured/Bundle/index.html new file mode 100644 index 00000000..d3e089fa --- /dev/null +++ b/picos_std/Picos_std_structured/Bundle/index.html @@ -0,0 +1,6 @@ + +Bundle (picos_std.Picos_std_structured.Bundle)

Module Picos_std_structured.Bundle

An explicit dynamic bundle of fibers guaranteed to be joined at the end.

Bundles allow you to conveniently structure or delimit concurrency into nested scopes. After a bundle returns or raises an exception, no fibers forked to the bundle remain.

An unhandled exception, or error, within any fiber of the bundle causes all of the fibers forked to the bundle to be canceled and the bundle to raise the error exception or error exceptions raised by all of the fibers forked into the bundle.

type t

Represents a bundle of fibers.

val join_after : + ?callstack:int -> + ?on_return:[ `Terminate | `Wait ] -> + (t -> 'a) -> + 'a

join_after scope calls scope with a bundle. A call of join_after returns or raises only after scope has returned or raised and all forked fibers have terminated. If scope raises an exception, error will be called.

The optional on_return argument specifies what to do when the scope returns normally. It defaults to `Wait, which means to just wait for all the fibers to terminate on their own. When explicitly specified as ~on_return:`Terminate, then terminate ?callstack will be called on return. This can be convenient, for example, when dealing with daemon fibers.

val terminate : ?callstack:int -> t -> unit

terminate bundle cancels all of the forked fibers using the Terminate exception. After terminate has been called, no new fibers can be forked to the bundle.

The optional callstack argument specifies the number of callstack entries to capture with the Terminate exception. The default is 0.

ℹ️ Calling terminate at the end of a bundle can be a convenient way to cancel any background fibers started by the bundle.

ℹ️ Calling terminate does not raise the Terminate exception, but blocking operations after terminate will raise the exception to propagate cancelation unless propagation of cancelation is forbidden.

val terminate_after : ?callstack:int -> t -> seconds:float -> unit

terminate_after ~seconds bundle arranges to terminate the bundle after the specified timeout in seconds.

val error : ?callstack:int -> t -> exn -> Stdlib.Printexc.raw_backtrace -> unit

error bundle exn bt first calls terminate and then adds the exception with backtrace to the list of exceptions to be raised, unless the exception is the Terminate exception, which is not considered to signal an error by itself.

The optional callstack argument is passed to terminate.

val fork_as_promise : t -> (unit -> 'a) -> 'a Promise.t

fork_as_promise bundle thunk spawns a new fiber to the bundle that will run the given thunk. The result of the thunk will be written to the promise. If the thunk raises an exception, error will be called with that exception.

val fork : t -> (unit -> unit) -> unit

fork bundle action is equivalent to fork_as_promise bundle action |> ignore.

diff --git a/picos_std/Picos_std_structured/Control/index.html b/picos_std/Picos_std_structured/Control/index.html new file mode 100644 index 00000000..aefc3606 --- /dev/null +++ b/picos_std/Picos_std_structured/Control/index.html @@ -0,0 +1,17 @@ + +Control (picos_std.Picos_std_structured.Control)

Module Picos_std_structured.Control

Basic control operations and exceptions for structured concurrency.

exception Terminate

An exception that is used to signal fibers, typically by canceling them, that they should terminate by letting the exception propagate.

ℹ️ Within this library, the Terminate exception does not, by itself, indicate an error. Raising it inside a fiber forked within the structured concurrency constructs of this library simply causes the relevant part of the tree of fibers to be terminated.

⚠️ If Terminate is raised in the main fiber of a Bundle, and no other exceptions are raised within any fiber inside the bundle, the bundle will then, of course, raise the Terminate exception after all of the fibers have been terminated.

exception Errors of (exn * Stdlib.Printexc.raw_backtrace) list

An exception that can be used to collect exceptions, typically indicating errors, from multiple fibers.

ℹ️ The Terminate exception is not considered an error within this library and the structuring constructs do not include it in the list of Errors.

val raise_if_canceled : unit -> unit

raise_if_canceled () checks whether the current fiber has been canceled and if so raises the exception that the fiber was canceled with.

ℹ️ Within this library fibers are canceled using the Terminate exception.

val yield : unit -> unit

yield () asks the current fiber to be rescheduled.

val sleep : seconds:float -> unit

sleep ~seconds suspends the current fiber for the specified number of seconds.

val protect : (unit -> 'a) -> 'a

protect thunk forbids propagation of cancelation for the duration of thunk ().

ℹ️ With the constructs provided by this library it is not possible to prevent a fiber from being canceled, but it is possible for a fiber to forbid the scheduler from propagating cancelation to the fiber.

val block : unit -> 'a

block () suspends the current fiber until it is canceled at which point the cancelation exception will be raised.

  • raises Invalid_argument

    in case propagation of cancelation has been forbidden.

  • raises Sys_error

    in case the underlying computation of the fiber is forced to return during block. This is only possible when the fiber has been spawned through another library.

val terminate_after : ?callstack:int -> seconds:float -> (unit -> 'a) -> 'a

terminate_after ~seconds thunk arranges to terminate the execution of thunk on the current fiber after the specified timeout in seconds.

Using terminate_after one can attempt any blocking operation that supports cancelation with a timeout. For example, one could try to read an Ivar with a timeout

let peek_in ~seconds ivar =
+  match
+    Control.terminate_after ~seconds @@ fun () ->
+      Ivar.read ivar
+  with
+  | value -> Some value
+  | exception Control.Terminate -> None

or one could try to connect a socket with a timeout

let try_connect_in ~seconds socket sockaddr =
+  match
+    Control.terminate_after ~seconds @@ fun () ->
+      Unix.connect socket sockaddr
+  with
+  | () -> true
+  | exception Control.Terminate -> false

using the Picos_io.Unix module.

The optional callstack argument specifies the number of callstack entries to capture with the Terminate exception. The default is 0.

As an example, terminate_after could be implemented using Bundle as follows:

let terminate_after ?callstack ~seconds thunk =
+  Bundle.join_after @@ fun bundle ->
+  Bundle.terminate_after ?callstack ~seconds bundle;
+  thunk ()
diff --git a/picos_std/Picos_std_structured/Flock/index.html b/picos_std/Picos_std_structured/Flock/index.html new file mode 100644 index 00000000..bb100f87 --- /dev/null +++ b/picos_std/Picos_std_structured/Flock/index.html @@ -0,0 +1,6 @@ + +Flock (picos_std.Picos_std_structured.Flock)

Module Picos_std_structured.Flock

An implicit dynamic flock of fibers guaranteed to be joined at the end.

Flocks allow you to conveniently structure or delimit concurrency into nested scopes. After a flock returns or raises an exception, no fibers forked to the flock remain.

An unhandled exception, or error, within any fiber of the flock causes all of the fibers forked to the flock to be canceled and the flock to raise the error exception or error exceptions raised by all of the fibers forked into the flock.

ℹ️ This is essentially a very thin convenience wrapper for an implicitly propagated Bundle.

⚠️ All of the operations in this module, except join_after, raise the Invalid_argument exception in case they are called from outside of the dynamic multifiber scope of a flock established by calling join_after.

val join_after : + ?callstack:int -> + ?on_return:[ `Terminate | `Wait ] -> + (unit -> 'a) -> + 'a

join_after scope creates a new flock for fibers, calls scope after setting current flock to the new flock, and restores the previous flock, if any after scope exits. The flock will be implicitly propagated to all fibers forked into the flock. A call of join_after returns or raises only after scope has returned or raised and all forked fibers have terminated. If scope raises an exception, error will be called.

The optional on_return argument specifies what to do when the scope returns normally. It defaults to `Wait, which means to just wait for all the fibers to terminate on their own. When explicitly specified as ~on_return:`Terminate, then terminate ?callstack will be called on return. This can be convenient, for example, when dealing with daemon fibers.

val terminate : ?callstack:int -> unit -> unit

terminate () cancels all of the forked fibers using the Terminate exception. After terminate has been called, no new fibers can be forked to the current flock.

The optional callstack argument specifies the number of callstack entries to capture with the Terminate exception. The default is 0.

ℹ️ Calling terminate at the end of a flock can be a convenient way to cancel any background fibers started by the flock.

ℹ️ Calling terminate does not raise the Terminate exception, but blocking operations after terminate will raise the exception to propagate cancelation unless propagation of cancelation is forbidden.

val terminate_after : ?callstack:int -> seconds:float -> unit -> unit

terminate_after ~seconds () arranges to terminate the current flock after the specified timeout in seconds.

val error : ?callstack:int -> exn -> Stdlib.Printexc.raw_backtrace -> unit

error exn bt first calls terminate and then adds the exception with backtrace to the list of exceptions to be raised, unless the exception is the Terminate exception, which is not considered to signal an error by itself.

The optional callstack argument is passed to terminate.

val fork_as_promise : (unit -> 'a) -> 'a Promise.t

fork_as_promise thunk spawns a new fiber to the current flock that will run the given thunk. The result of the thunk will be written to the promise. If the thunk raises an exception, error will be called with that exception.

val fork : (unit -> unit) -> unit

fork action is equivalent to fork_as_promise action |> ignore.

diff --git a/picos_std/Picos_std_structured/Promise/index.html b/picos_std/Picos_std_structured/Promise/index.html new file mode 100644 index 00000000..38060241 --- /dev/null +++ b/picos_std/Picos_std_structured/Promise/index.html @@ -0,0 +1,2 @@ + +Promise (picos_std.Picos_std_structured.Promise)

Module Picos_std_structured.Promise

A cancelable promise.

ℹ️ In addition to using a promise to concurrently compute and return a value, a cancelable promise can also represent a concurrent fiber that will continue until it is explicitly canceled.

type !'a t

Represents a promise to return value of type 'a.

val of_value : 'a -> 'a t

of_value value returns a constant completed promise that returns the given value.

ℹ️ Promises can also be created in the scope of a Bundle or a Flock.

val await : 'a t -> 'a

await promise awaits until the promise has completed and either returns the value that the evaluation of the promise returned, raises the exception that the evaluation of the promise raised, or raises the Terminate exception in case the promise has been canceled.

val completed : 'a t -> 'a Picos_std_event.Event.t

completed promise returns an event that can be committed to once the promise has completed.

val is_running : 'a t -> bool

is_running promise determines whether the completion of the promise is still pending.

val try_terminate : ?callstack:int -> 'a t -> bool

try_terminate promise tries to terminate the promise by canceling it with the Terminate exception and returns true in case of success and false in case the promise had already completed, i.e. either returned, raised, or canceled.

The optional callstack argument specifies the number of callstack entries to capture with the Terminate exception. The default is 0.

val terminate : ?callstack:int -> 'a t -> unit

terminate promise is equivalent to try_terminate promise |> ignore.

val terminate_after : ?callstack:int -> 'a t -> seconds:float -> unit

terminate_after ~seconds promise arranges to terminate the promise by canceling it with the Terminate exception after the specified timeout in seconds.

The optional callstack argument specifies the number of callstack entries to capture with the Terminate exception. The default is 0.

diff --git a/picos_std/Picos_std_structured/Run/index.html b/picos_std/Picos_std_structured/Run/index.html new file mode 100644 index 00000000..840e5b1c --- /dev/null +++ b/picos_std/Picos_std_structured/Run/index.html @@ -0,0 +1,12 @@ + +Run (picos_std.Picos_std_structured.Run)

Module Picos_std_structured.Run

Operations for running fibers in specific patterns.

val all : (unit -> unit) list -> unit

all actions starts the actions as separate fibers and waits until they all complete or one of them raises an unhandled exception other than Terminate, which is not counted as an error, after which the remaining fibers will be canceled.

⚠️ One of actions may be run on the current fiber.

⚠️ It is not guaranteed that any of the actions in the list are called. In particular, after any action raises an unhandled exception or after the main fiber is canceled, the actions that have not yet started may be skipped entirely.

all is roughly equivalent to

let all actions =
+  Bundle.join_after @@ fun bundle ->
+  List.iter (Bundle.fork bundle) actions

but treats the list of actions as a single computation.

val any : (unit -> unit) list -> unit

any actions starts the actions as separate fibers and waits until one of them completes or raises an unhandled exception other than Terminate, which is not counted as an error, after which the rest of the started fibers will be canceled.

⚠️ One of actions may be run on the current fiber.

⚠️ It is not guaranteed that any of the actions in the list are called. In particular, after the first action returns successfully or after any action raises an unhandled exception or after the main fiber is canceled, the actions that have not yet started may be skipped entirely.

any is roughly equivalent to

let any actions =
+  Bundle.join_after @@ fun bundle ->
+  try
+    actions
+    |> List.iter @@ fun action ->
+       Bundle.fork bundle @@ fun () ->
+       action ();
+       Bundle.terminate bundle
+  with Control.Terminate -> ()

but treats the list of actions as a single computation.

diff --git a/picos_std/Picos_std_structured/index.html b/picos_std/Picos_std_structured/index.html new file mode 100644 index 00000000..987268a0 --- /dev/null +++ b/picos_std/Picos_std_structured/index.html @@ -0,0 +1,189 @@ + +Picos_std_structured (picos_std.Picos_std_structured)

Module Picos_std_structured

Basic structured concurrency primitives for Picos.

This library essentially provides one application programming interface for structuring fibers with any Picos compatible scheduler.

For the examples we open some modules:

open Picos_io
+open Picos_std_event
+open Picos_std_finally
+open Picos_std_structured
+open Picos_std_sync

Modules

module Control : sig ... end

Basic control operations and exceptions for structured concurrency.

module Promise : sig ... end

A cancelable promise.

module Bundle : sig ... end

An explicit dynamic bundle of fibers guaranteed to be joined at the end.

module Flock : sig ... end

An implicit dynamic flock of fibers guaranteed to be joined at the end.

module Run : sig ... end

Operations for running fibers in specific patterns.

Examples

Understanding cancelation

Consider the following program:

let main () =
+  Flock.join_after begin fun () ->
+    let promise =
+      Flock.fork_as_promise @@ fun () ->
+      Control.block ()
+    in
+
+    Flock.fork begin fun () ->
+      Promise.await promise
+    end;
+
+    Flock.fork begin fun () ->
+      let condition = Condition.create ()
+      and mutex = Mutex.create () in
+      Mutex.protect mutex begin fun () ->
+        while true do
+          Condition.wait condition mutex
+        done
+      end
+    end;
+
+    Flock.fork begin fun () ->
+      let sem =
+        Semaphore.Binary.make false
+      in
+      Semaphore.Binary.acquire sem
+    end;
+
+    Flock.fork begin fun () ->
+      let sem =
+        Semaphore.Counting.make 0
+      in
+      Semaphore.Counting.acquire sem
+    end;
+
+    Flock.fork begin fun () ->
+      Event.sync (Event.choose [])
+    end;
+
+    Flock.fork begin fun () ->
+      let latch = Latch.create 1 in
+      Latch.await latch
+    end;
+
+    Flock.fork begin fun () ->
+      let ivar = Ivar.create () in
+      Ivar.read ivar
+    end;
+
+    Flock.fork begin fun () ->
+      let stream = Stream.create () in
+      Stream.read (Stream.tap stream)
+      |> ignore
+    end;
+
+    Flock.fork begin fun () ->
+      let@ inn, out = finally
+        Unix.close_pair @@ fun () ->
+        Unix.socketpair ~cloexec:true
+          PF_UNIX SOCK_STREAM 0
+      in
+      Unix.set_nonblock inn;
+      let n =
+        Unix.read inn (Bytes.create 1)
+          0 1
+      in
+      assert (n = 1)
+    end;
+
+    Flock.fork begin fun () ->
+      let a_month =
+        60.0 *. 60.0 *. 24.0 *. 30.0
+      in
+      Control.sleep ~seconds:a_month
+    end;
+
+    (* Let the children get stuck *)
+    Control.sleep ~seconds:0.1;
+
+    Flock.terminate ()
+  end

First of all, note that above the Mutex, Condition, and Semaphore modules come from the Picos_std_sync library and the Unix module comes from the Picos_io library. They do not come from the standard OCaml libraries.

The above program creates a flock of fibers and forks several fibers to the flock that all block in various ways. In detail,

Fibers forked to a flock can be canceled in various ways. In the above program we call Flock.terminate to cancel all of the fibers and effectively close the flock. This allows the program to return normally immediately and without leaking or leaving anything in an invalid state:

# Picos_mux_random.run_on ~n_domains:2 main
+- : unit = ()

Now, the point of the above example isn't that you should just call terminate when your program gets stuck. 😅

What the above example hopefully demonstrates is that concurrent abstractions like mutexes and condition variables, asynchronous IO libraries, and others can be designed to support cancelation.

Cancelation is a signaling mechanism that allows structured concurrent abstractions, like the Flock abstraction, to (hopefully) gracefully tear down concurrent fibers in case of errors. Indeed, one of the basic ideas behind the Flock abstraction is that in case any fiber forked to the flock raises an unhandled exception, the whole flock will be terminated and the error will raised from the flock, which allows you to understand what went wrong, instead of having to debug a program that mysteriously gets stuck, for example.

Cancelation can also, with some care, be used as a mechanism to terminate fibers once they are no longer needed. However, just like sleep, for example, cancelation is inherently prone to races, i.e. it is difficult to understand the exact point and state at which a fiber gets canceled and it is usually non-deterministic, and therefore cancelation is not recommended for use as a general synchronization or communication mechanism.

Errors and cancelation

Consider the following program:

let many_errors () =
+  Flock.join_after @@ fun () ->
+
+  let latch = Latch.create 1 in
+
+  let fork_raising exn =
+    Flock.fork begin fun () ->
+      Control.protect begin fun () ->
+        Latch.await latch
+      end;
+      raise exn
+    end
+  in
+
+  fork_raising Exit;
+  fork_raising Not_found;
+  fork_raising Control.Terminate;
+
+  Latch.decr latch

The above program starts three fibers and uses a latch to ensure that all of them have been started, before two of them raise errors and the third raises Terminate, which is not considered an error in this library. Running the program

# Picos_mux_fifo.run many_errors
+Exception: Errors[Stdlib.Exit; Not_found]

raises a collection of all of the errors.

A simple echo server and clients

Let's build a simple TCP echo server and run it with some clients.

We first define a function for the server:

let run_server server_fd =
+  Flock.join_after begin fun () ->
+    while true do
+      let@ client_fd =
+        instantiate Unix.close @@ fun () ->
+        Unix.accept
+          ~cloexec:true server_fd |> fst
+      in
+
+      (* Fork a fiber for client *)
+      Flock.fork begin fun () ->
+        let@ client_fd =
+          move client_fd
+        in
+        Unix.set_nonblock client_fd;
+
+        let bs = Bytes.create 100 in
+        let n =
+          Unix.read client_fd bs 0
+            (Bytes.length bs)
+        in
+        Unix.write client_fd bs 0 n
+        |> ignore
+      end
+    done
+  end

The server function expects a listening socket. For each accepted client the server forks a new fiber to handle it. The client socket is moved from the server fiber to the client fiber to avoid leaks and to ensure that the socket will be closed.

Let's then define a function for the clients:

let run_client server_addr =
+  let@ socket =
+    finally Unix.close @@ fun () ->
+    Unix.socket ~cloexec:true
+      PF_INET SOCK_STREAM 0
+  in
+  Unix.set_nonblock socket;
+  Unix.connect socket server_addr;
+
+  let msg = "Hello!" in
+  Unix.write_substring
+    socket msg 0 (String.length msg)
+  |> ignore;
+
+  let bytes =
+    Bytes.create (String.length msg)
+  in
+  let n =
+    Unix.read socket bytes 0
+      (Bytes.length bytes)
+  in
+
+  Printf.printf "Received: %s\n%!"
+    (Bytes.sub_string bytes 0 n)

The client function takes the address of the server and connects a socket to the server address. It then writes a message to the server and reads a reply from the server and prints it.

Here is the main program:

let main () =
+  let@ server_fd =
+    finally Unix.close @@ fun () ->
+    Unix.socket ~cloexec:true
+      PF_INET SOCK_STREAM 0
+  in
+  Unix.set_nonblock server_fd;
+  (* Let system determine the port *)
+  Unix.bind server_fd Unix.(
+    ADDR_INET(inet_addr_loopback, 0));
+  Unix.listen server_fd 8;
+
+  let server_addr =
+    Unix.getsockname server_fd
+  in
+
+  Flock.join_after ~on_return:`Terminate begin fun () ->
+    (* Start server *)
+    Flock.fork begin fun () ->
+      run_server server_fd
+    end;
+
+    (* Run clients concurrently *)
+    Flock.join_after begin fun () ->
+      for _ = 1 to 5 do
+        Flock.fork @@ fun () ->
+          run_client server_addr
+      done
+    end
+  end

The main program creates a socket for the server and configures it. The server is then started as a fiber in a flock terminated on return. Then the clients are started to run concurrently in an inner flock.

Finally we run the main program with a scheduler:

# Picos_mux_random.run_on ~n_domains:1 main
+Received: Hello!
+Received: Hello!
+Received: Hello!
+Received: Hello!
+Received: Hello!
+- : unit = ()

As an exercise, you might want to refactor the server to avoid moving the file descriptors and use a recursive accept loop instead. You could also terminate the whole flock at the end instead of just terminating the server.

diff --git a/picos_std/Picos_std_structured__/index.html b/picos_std/Picos_std_structured__/index.html new file mode 100644 index 00000000..e9ecd0a6 --- /dev/null +++ b/picos_std/Picos_std_structured__/index.html @@ -0,0 +1,2 @@ + +Picos_std_structured__ (picos_std.Picos_std_structured__)

Module Picos_std_structured__

This module is hidden.

diff --git a/picos_std/Picos_std_structured__Bundle/index.html b/picos_std/Picos_std_structured__Bundle/index.html new file mode 100644 index 00000000..3a5e3336 --- /dev/null +++ b/picos_std/Picos_std_structured__Bundle/index.html @@ -0,0 +1,2 @@ + +Picos_std_structured__Bundle (picos_std.Picos_std_structured__Bundle)

Module Picos_std_structured__Bundle

This module is hidden.

diff --git a/picos_std/Picos_std_structured__Control/index.html b/picos_std/Picos_std_structured__Control/index.html new file mode 100644 index 00000000..df2a275e --- /dev/null +++ b/picos_std/Picos_std_structured__Control/index.html @@ -0,0 +1,2 @@ + +Picos_std_structured__Control (picos_std.Picos_std_structured__Control)

Module Picos_std_structured__Control

This module is hidden.

diff --git a/picos_std/Picos_std_structured__Flock/index.html b/picos_std/Picos_std_structured__Flock/index.html new file mode 100644 index 00000000..a8f3c3be --- /dev/null +++ b/picos_std/Picos_std_structured__Flock/index.html @@ -0,0 +1,2 @@ + +Picos_std_structured__Flock (picos_std.Picos_std_structured__Flock)

Module Picos_std_structured__Flock

This module is hidden.

diff --git a/picos_std/Picos_std_structured__Promise/index.html b/picos_std/Picos_std_structured__Promise/index.html new file mode 100644 index 00000000..2b303d8b --- /dev/null +++ b/picos_std/Picos_std_structured__Promise/index.html @@ -0,0 +1,2 @@ + +Picos_std_structured__Promise (picos_std.Picos_std_structured__Promise)

Module Picos_std_structured__Promise

This module is hidden.

diff --git a/picos_std/Picos_std_structured__Run/index.html b/picos_std/Picos_std_structured__Run/index.html new file mode 100644 index 00000000..cf37b10d --- /dev/null +++ b/picos_std/Picos_std_structured__Run/index.html @@ -0,0 +1,2 @@ + +Picos_std_structured__Run (picos_std.Picos_std_structured__Run)

Module Picos_std_structured__Run

This module is hidden.

diff --git a/picos_std/Picos_std_sync/Condition/index.html b/picos_std/Picos_std_sync/Condition/index.html new file mode 100644 index 00000000..c18e7370 --- /dev/null +++ b/picos_std/Picos_std_sync/Condition/index.html @@ -0,0 +1,2 @@ + +Condition (picos_std.Picos_std_sync.Condition)

Module Picos_std_sync.Condition

A condition variable.

ℹ️ This intentionally mimics the interface of Stdlib.Condition. Unlike with the standard library condition variable, blocking on this condition variable allows an effects based scheduler to run other fibers on the thread.

type t

Represents a condition variable.

val create : ?padded:bool -> unit -> t

create () return a new condition variable.

val wait : t -> Mutex.t -> unit

wait condition unlocks the mutex, waits for the condition, and locks the mutex before returning or raising due to the operation being canceled.

ℹ️ If the fiber has been canceled and propagation of cancelation is allowed, this may raise the cancelation exception.

val signal : t -> unit

signal condition wakes up one fiber waiting on the condition variable unless there are no such fibers.

val broadcast : t -> unit

broadcast condition wakes up all the fibers waiting on the condition variable.

diff --git a/picos_std/Picos_std_sync/Ivar/index.html b/picos_std/Picos_std_sync/Ivar/index.html new file mode 100644 index 00000000..64417499 --- /dev/null +++ b/picos_std/Picos_std_sync/Ivar/index.html @@ -0,0 +1,2 @@ + +Ivar (picos_std.Picos_std_sync.Ivar)

Module Picos_std_sync.Ivar

An incremental or single-assignment poisonable variable.

type !'a t

Represents an incremental variable.

val create : unit -> 'a t

create () returns a new empty incremental variable.

val of_value : 'a -> 'a t

of_value value returns an incremental variable prefilled with the given value.

val try_fill : 'a t -> 'a -> bool

try_fill ivar value attempts to assign the given value to the incremental variable. Returns true on success and false in case the variable had already been poisoned or assigned a value.

val fill : 'a t -> 'a -> unit

fill ivar value is equivalent to try_fill ivar value |> ignore.

val try_poison : 'a t -> exn -> Stdlib.Printexc.raw_backtrace -> bool

try_poison ivar exn bt attempts to poison the incremental variable with the specified exception and backtrace. Returns true on success and false in case the variable had already been poisoned or assigned a value.

val poison : 'a t -> exn -> Stdlib.Printexc.raw_backtrace -> unit

poison ivar exn bt is equivalent to try_poison ivar exn bt |> ignore.

val peek_opt : 'a t -> 'a option

peek_opt ivar either returns Some value in case the variable has been assigned the value, raises an exception in case the variable has been poisoned, or otherwise returns None, which means that the variable has not yet been poisoned or assigned a value.

val read : 'a t -> 'a

read ivar waits until the variable is either assigned a value or the variable is poisoned and then returns the value or raises the exception.

val read_evt : 'a t -> 'a Picos_std_event.Event.t

read_evt ivar returns an event that can be committed to once the variable has either been assigned a value or has been poisoned.

diff --git a/picos_std/Picos_std_sync/Latch/index.html b/picos_std/Picos_std_sync/Latch/index.html new file mode 100644 index 00000000..fee85e31 --- /dev/null +++ b/picos_std/Picos_std_sync/Latch/index.html @@ -0,0 +1,4 @@ + +Latch (picos_std.Picos_std_sync.Latch)

Module Picos_std_sync.Latch

A dynamic single-use countdown latch.

Latches are typically used for determining when a finite set of parallel computations is done. If the size of the set is known a priori, then the latch can be initialized with the size as initial count and then each computation just decrements the latch.

If the size is unknown, i.e. it is determined dynamically, then a latch is initialized with a count of one, the a priori known computations are started and then the latch is decremented. When a computation is stsrted, the latch is incremented, and then decremented once the computation has finished.

type t

Represents a dynamic countdown latch.

val create : ?padded:bool -> int -> t

create initial creates a new countdown latch with the specified initial count.

  • raises Invalid_argument

    in case the specified initial count is negative.

val try_decr : t -> bool

try_decr latch attempts to decrement the count of the latch and returns true in case the count of the latch was greater than zero and false in case the count already was zero.

val decr : t -> unit

decr latch is equivalent to:

if not (try_decr latch) then
+  invalid_arg "zero count"
  • raises Invalid_argument

    in case the count of the latch is zero.

val try_incr : t -> bool

try_incr latch attempts to increment the count of the latch and returns true on success and false on failure, which means that the latch has already reached zero.

val incr : t -> unit

incr latch is equivalent to:

if not (try_incr latch) then
+  invalid_arg "zero count"
  • raises Invalid_argument

    in case the count of the latch is zero.

val await : t -> unit

await latch returns after the count of the latch has reached zero.

val await_evt : t -> unit Picos_std_event.Event.t

await_evt latch returns an event that can be committed to once the count of the latch has reached zero.

diff --git a/picos_std/Picos_std_sync/Lazy/index.html b/picos_std/Picos_std_sync/Lazy/index.html new file mode 100644 index 00000000..13c9575b --- /dev/null +++ b/picos_std/Picos_std_sync/Lazy/index.html @@ -0,0 +1,5 @@ + +Lazy (picos_std.Picos_std_sync.Lazy)

Module Picos_std_sync.Lazy

A lazy suspension.

ℹ️ This intentionally mimics the interface of Stdlib.Lazy. Unlike with the standard library suspensions an attempt to force a suspension from multiple fibers, possibly running on different domains, does not raise the Undefined exception.

exception Undefined
type !'a t

Represents a deferred computation or suspension.

val from_fun : (unit -> 'a) -> 'a t

from_fun thunk returns a suspension.

val from_val : 'a -> 'a t

from_val value returns an already forced suspension whose result is the given value.

val is_val : 'a t -> bool

is_val susp determines whether the suspension has already been forced and didn't raise an exception.

val force : 'a t -> 'a

force susp forces the suspension, i.e. computes thunk () using the thunk passed to from_fun, stores the result of the computation to the suspension and reproduces its result. In case the suspension has already been forced the computation is skipped and stored result is reproduced.

ℹ️ This will check whether the current fiber has been canceled before starting the computation of thunk (). This allows the suspension to be forced by another fiber. However, if the fiber is canceled and the cancelation exception is raised after the computation has been started, the suspension will then store the cancelation exception.

  • raises Undefined

    in case the suspension is currently being forced by the current fiber.

val force_val : 'a t -> 'a

force_val is a synonym for force.

val map : ('a -> 'b) -> 'a t -> 'b t

map fn susp is equivalent to from_fun (fun () -> fn (force susp)).

val map_val : ('a -> 'b) -> 'a t -> 'b t

map_val fn susp is equivalent to:

if is_val susp then
+  from_val (fn (force susp))
+else
+  map fn susp
diff --git a/picos_std/Picos_std_sync/Mutex/index.html b/picos_std/Picos_std_sync/Mutex/index.html new file mode 100644 index 00000000..f16a567b --- /dev/null +++ b/picos_std/Picos_std_sync/Mutex/index.html @@ -0,0 +1,2 @@ + +Mutex (picos_std.Picos_std_sync.Mutex)

Module Picos_std_sync.Mutex

A mutual-exclusion lock or mutex.

ℹ️ This intentionally mimics the interface of Stdlib.Mutex. Unlike with the standard library mutex, blocking on this mutex potentially allows an effects based scheduler to run other fibers on the thread.

🏎️ The optional checked argument taken by most of the operations defaults to true. When explicitly specified as ~checked:false the mutex implementation may avoid having to obtain the current fiber, which can be expensive relative to locking or unlocking an uncontested mutex. Note that specifying ~checked:false on an operation may prevent error checking also on a subsequent operation.

type t

Represents a mutual-exclusion lock or mutex.

val create : ?padded:bool -> unit -> t

create () returns a new mutex that is initially unlocked.

val lock : ?checked:bool -> t -> unit

lock mutex locks the mutex.

ℹ️ If the fiber has been canceled and propagation of cancelation is allowed, this may raise the cancelation exception before locking the mutex. If ~checked:false was specified, the cancelation exception may or may not be raised.

  • raises Sys_error

    if the mutex is already locked by the fiber. If ~checked:false was specified for some previous operation on the mutex the exception may or may not be raised.

val try_lock : ?checked:bool -> t -> bool

try_lock mutex locks the mutex in case the mutex is unlocked. Returns true on success and false in case the mutex was locked.

ℹ️ If the fiber has been canceled and propagation of cancelation is allowed, this may raise the cancelation exception before locking the mutex. If ~checked:false was specified, the cancelation exception may or may not be raised.

val unlock : ?checked:bool -> t -> unit

unlock mutex unlocks the mutex.

  • raises Sys_error

    if the mutex was locked by another fiber. If ~checked:false was specified for some previous operation on the mutex the exception may or may not be raised.

val protect : ?checked:bool -> t -> (unit -> 'a) -> 'a

protect mutex thunk locks the mutex, runs thunk (), and unlocks the mutex after thunk () returns or raises.

ℹ️ If the fiber has been canceled and propagation of cancelation is allowed, this may raise the cancelation exception before locking the mutex. If ~checked:false was specified, the cancelation exception may or may not be raised.

  • raises Sys_error

    for the same reasons as lock and unlock.

diff --git a/picos_std/Picos_std_sync/Semaphore/Binary/index.html b/picos_std/Picos_std_sync/Semaphore/Binary/index.html new file mode 100644 index 00000000..265f162c --- /dev/null +++ b/picos_std/Picos_std_sync/Semaphore/Binary/index.html @@ -0,0 +1,2 @@ + +Binary (picos_std.Picos_std_sync.Semaphore.Binary)

Module Semaphore.Binary

A binary semaphore.

type t

Represents a binary semaphore.

val make : ?padded:bool -> bool -> t

make initial creates a new binary semaphore with count of 1 in case initial is true and count of 0 otherwise.

val release : t -> unit

release semaphore sets the count of the semaphore to 1.

val acquire : t -> unit

acquire semaphore waits until the count of the semaphore is 1 and then atomically changes the count to 0.

val try_acquire : t -> bool

try_acquire semaphore attempts to atomically change the count of the semaphore from 1 to 0.

diff --git a/picos_std/Picos_std_sync/Semaphore/Counting/index.html b/picos_std/Picos_std_sync/Semaphore/Counting/index.html new file mode 100644 index 00000000..fb03e5e8 --- /dev/null +++ b/picos_std/Picos_std_sync/Semaphore/Counting/index.html @@ -0,0 +1,2 @@ + +Counting (picos_std.Picos_std_sync.Semaphore.Counting)

Module Semaphore.Counting

A counting semaphore.

type t

Represents a counting semaphore.

val make : ?padded:bool -> int -> t

make initial creates a new counting semaphore with the given initial count.

  • raises Invalid_argument

    in case the given initial count is negative.

val release : t -> unit

release semaphore increments the count of the semaphore.

  • raises Sys_error

    in case the count would overflow.

val acquire : t -> unit

acquire semaphore waits until the count of the semaphore is greater than 0 and then atomically decrements the count.

val try_acquire : t -> bool

try_acquire semaphore attempts to atomically decrement the count of the semaphore unless the count is already 0.

val get_value : t -> int

get_value semaphore returns the current count of the semaphore. This should only be used for debugging or informational messages.

diff --git a/picos_std/Picos_std_sync/Semaphore/index.html b/picos_std/Picos_std_sync/Semaphore/index.html new file mode 100644 index 00000000..e580a464 --- /dev/null +++ b/picos_std/Picos_std_sync/Semaphore/index.html @@ -0,0 +1,2 @@ + +Semaphore (picos_std.Picos_std_sync.Semaphore)

Module Picos_std_sync.Semaphore

Counting and Binary semaphores.

ℹ️ This intentionally mimics the interface of Stdlib.Semaphore. Unlike with the standard library semaphores, blocking on these semaphores allows an effects based scheduler to run other fibers on the thread.

module Counting : sig ... end

A counting semaphore.

module Binary : sig ... end

A binary semaphore.

diff --git a/picos_std/Picos_std_sync/Stream/index.html b/picos_std/Picos_std_sync/Stream/index.html new file mode 100644 index 00000000..8caea47c --- /dev/null +++ b/picos_std/Picos_std_sync/Stream/index.html @@ -0,0 +1,2 @@ + +Stream (picos_std.Picos_std_sync.Stream)

Module Picos_std_sync.Stream

A lock-free, poisonable, many-to-many, stream.

Readers can tap into a stream to get a cursor for reading all the values pushed to the stream starting from the cursor position. Conversely, values pushed to a stream are lost unless a reader has a cursor to the position in the stream.

type !'a t

Represents a stream of values of type 'a.

val create : ?padded:bool -> unit -> 'a t

create () returns a new stream.

val push : 'a t -> 'a -> unit

push stream value adds the value to the current position of the stream and advances the stream to the next position unless the stream has been poisoned in which case only the exception given to poison will be raised.

val poison : 'a t -> exn -> Stdlib.Printexc.raw_backtrace -> unit

poison stream exn bt marks the stream as poisoned at the current position, which means that subsequent attempts to push to the stream will raise the given exception with backtrace.

type !'a cursor

Represents a (past or current) position in a stream.

val tap : 'a t -> 'a cursor

tap stream returns a cursor to the current position of the stream.

val peek_opt : 'a cursor -> ('a * 'a cursor) option

peek_opt cursor immediately returns Some (value, next) with the value pushed to the position and a cursor to the next position, when the cursor points to a past position in the stream. Otherwise returns None or raises the exception that the stream was poisoned with.

val read : 'a cursor -> 'a * 'a cursor

read cursor immediately returns (value, next) with the value pushed to the position and a cursor to the next position, when the cursor points to a past position in the stream. If the cursor points to the current position of the stream, read cursor waits until a value is pushed to the stream or the stream is poisoned, in which case the exception that the stream was poisoned with will be raised.

val read_evt : 'a cursor -> ('a * 'a cursor) Picos_std_event.Event.t

read_evt cursor returns an event that reads from the cursor position.

diff --git a/picos_std/Picos_std_sync/index.html b/picos_std/Picos_std_sync/index.html new file mode 100644 index 00000000..f9cbd3d2 --- /dev/null +++ b/picos_std/Picos_std_sync/index.html @@ -0,0 +1,98 @@ + +Picos_std_sync (picos_std.Picos_std_sync)

Module Picos_std_sync

Basic communication and synchronization primitives for Picos.

This library essentially provides a conventional set of communication and synchronization primitives for concurrent programming with any Picos compatible scheduler.

For the examples we open some modules:

open Picos_std_structured
+open Picos_std_sync

Modules

module Mutex : sig ... end

A mutual-exclusion lock or mutex.

module Condition : sig ... end

A condition variable.

module Semaphore : sig ... end

Counting and Binary semaphores.

module Lazy : sig ... end

A lazy suspension.

module Latch : sig ... end

A dynamic single-use countdown latch.

module Ivar : sig ... end

An incremental or single-assignment poisonable variable.

module Stream : sig ... end

A lock-free, poisonable, many-to-many, stream.

Examples

A simple bounded queue

Here is an example of a simple bounded (blocking) queue using a mutex and condition variables:

module Bounded_q : sig
+  type 'a t
+  val create : capacity:int -> 'a t
+  val push : 'a t -> 'a -> unit
+  val pop : 'a t -> 'a
+end = struct
+  type 'a t = {
+    mutex : Mutex.t;
+    queue : 'a Queue.t;
+    capacity : int;
+    not_empty : Condition.t;
+    not_full : Condition.t;
+  }
+
+  let create ~capacity =
+    if capacity < 0 then
+      invalid_arg "negative capacity"
+    else {
+      mutex = Mutex.create ();
+      queue = Queue.create ();
+      capacity;
+      not_empty = Condition.create ();
+      not_full = Condition.create ();
+    }
+
+  let is_full_unsafe t =
+    t.capacity <= Queue.length t.queue
+
+  let push t x =
+    let was_empty =
+      Mutex.protect t.mutex @@ fun () ->
+      while is_full_unsafe t do
+        Condition.wait t.not_full t.mutex
+      done;
+      Queue.push x t.queue;
+      Queue.length t.queue = 1
+    in
+    if was_empty then
+      Condition.broadcast t.not_empty
+
+  let pop t =
+    let elem, was_full =
+      Mutex.protect t.mutex @@ fun () ->
+      while Queue.length t.queue = 0 do
+        Condition.wait
+          t.not_empty t.mutex
+      done;
+      let was_full = is_full_unsafe t in
+      Queue.pop t.queue, was_full
+    in
+    if was_full then
+      Condition.broadcast t.not_full;
+    elem
+end

The above is definitely not the fastest nor the most scalable bounded queue, but we can now demonstrate it with the cooperative Picos_mux_fifo scheduler:

# Picos_mux_fifo.run @@ fun () ->
+
+  let bq =
+    Bounded_q.create ~capacity:3
+  in
+
+  Flock.join_after ~on_return:`Terminate begin fun () ->
+    Flock.fork begin fun () ->
+      while true do
+        Printf.printf "Popped %d\n%!"
+          (Bounded_q.pop bq)
+      done
+    end;
+
+    for i=1 to 5 do
+      Printf.printf "Pushing %d\n%!" i;
+      Bounded_q.push bq i
+    done;
+
+    Printf.printf "All done?\n%!";
+
+    Control.yield ();
+  end;
+
+  Printf.printf "Pushing %d\n%!" 101;
+  Bounded_q.push bq 101;
+
+  Printf.printf "Popped %d\n%!"
+    (Bounded_q.pop bq)
+Pushing 1
+Pushing 2
+Pushing 3
+Pushing 4
+Popped 1
+Popped 2
+Popped 3
+Pushing 5
+All done?
+Popped 4
+Popped 5
+Pushing 101
+Popped 101
+- : unit = ()

Notice how the producer was able to push three elements to the queue after which the fourth push blocked and the consumer was started. Also, after canceling the consumer, the queue could still be used just fine.

Conventions

The optional padded argument taken by several constructor functions, e.g. Latch.create, Mutex.create, Condition.create, Semaphore.Counting.make, and Semaphore.Binary.make, defaults to false. When explicitly specified as ~padded:true the object is allocated in a way to avoid false sharing. For relatively long lived objects this can improve performance and make performance more stable at the cost of using more memory. It is not recommended to use ~padded:true for short lived objects.

diff --git a/picos_std/Picos_std_sync__/index.html b/picos_std/Picos_std_sync__/index.html new file mode 100644 index 00000000..86005408 --- /dev/null +++ b/picos_std/Picos_std_sync__/index.html @@ -0,0 +1,2 @@ + +Picos_std_sync__ (picos_std.Picos_std_sync__)

Module Picos_std_sync__

This module is hidden.

diff --git a/picos_std/Picos_std_sync__Condition/index.html b/picos_std/Picos_std_sync__Condition/index.html new file mode 100644 index 00000000..94a20bfa --- /dev/null +++ b/picos_std/Picos_std_sync__Condition/index.html @@ -0,0 +1,2 @@ + +Picos_std_sync__Condition (picos_std.Picos_std_sync__Condition)

Module Picos_std_sync__Condition

This module is hidden.

diff --git a/picos_std/Picos_std_sync__Ivar/index.html b/picos_std/Picos_std_sync__Ivar/index.html new file mode 100644 index 00000000..4e61c774 --- /dev/null +++ b/picos_std/Picos_std_sync__Ivar/index.html @@ -0,0 +1,2 @@ + +Picos_std_sync__Ivar (picos_std.Picos_std_sync__Ivar)

Module Picos_std_sync__Ivar

This module is hidden.

diff --git a/picos_std/Picos_std_sync__Latch/index.html b/picos_std/Picos_std_sync__Latch/index.html new file mode 100644 index 00000000..ae8e49e9 --- /dev/null +++ b/picos_std/Picos_std_sync__Latch/index.html @@ -0,0 +1,2 @@ + +Picos_std_sync__Latch (picos_std.Picos_std_sync__Latch)

Module Picos_std_sync__Latch

This module is hidden.

diff --git a/picos_std/Picos_std_sync__Lazy/index.html b/picos_std/Picos_std_sync__Lazy/index.html new file mode 100644 index 00000000..a024ffe1 --- /dev/null +++ b/picos_std/Picos_std_sync__Lazy/index.html @@ -0,0 +1,2 @@ + +Picos_std_sync__Lazy (picos_std.Picos_std_sync__Lazy)

Module Picos_std_sync__Lazy

This module is hidden.

diff --git a/picos_std/Picos_std_sync__List_ext/index.html b/picos_std/Picos_std_sync__List_ext/index.html new file mode 100644 index 00000000..b3ee1448 --- /dev/null +++ b/picos_std/Picos_std_sync__List_ext/index.html @@ -0,0 +1,2 @@ + +Picos_std_sync__List_ext (picos_std.Picos_std_sync__List_ext)

Module Picos_std_sync__List_ext

This module is hidden.

diff --git a/picos_std/Picos_std_sync__Mutex/index.html b/picos_std/Picos_std_sync__Mutex/index.html new file mode 100644 index 00000000..d70e4778 --- /dev/null +++ b/picos_std/Picos_std_sync__Mutex/index.html @@ -0,0 +1,2 @@ + +Picos_std_sync__Mutex (picos_std.Picos_std_sync__Mutex)

Module Picos_std_sync__Mutex

This module is hidden.

diff --git a/picos_std/Picos_std_sync__Q/index.html b/picos_std/Picos_std_sync__Q/index.html new file mode 100644 index 00000000..b018b9a8 --- /dev/null +++ b/picos_std/Picos_std_sync__Q/index.html @@ -0,0 +1,2 @@ + +Picos_std_sync__Q (picos_std.Picos_std_sync__Q)

Module Picos_std_sync__Q

This module is hidden.

diff --git a/picos_std/Picos_std_sync__Semaphore/index.html b/picos_std/Picos_std_sync__Semaphore/index.html new file mode 100644 index 00000000..cb31cca9 --- /dev/null +++ b/picos_std/Picos_std_sync__Semaphore/index.html @@ -0,0 +1,2 @@ + +Picos_std_sync__Semaphore (picos_std.Picos_std_sync__Semaphore)

Module Picos_std_sync__Semaphore

This module is hidden.

diff --git a/picos_std/Picos_std_sync__Stream/index.html b/picos_std/Picos_std_sync__Stream/index.html new file mode 100644 index 00000000..f6685b9a --- /dev/null +++ b/picos_std/Picos_std_sync__Stream/index.html @@ -0,0 +1,2 @@ + +Picos_std_sync__Stream (picos_std.Picos_std_sync__Stream)

Module Picos_std_sync__Stream

This module is hidden.

diff --git a/picos_std/_doc-dir/CHANGES.md b/picos_std/_doc-dir/CHANGES.md new file mode 100644 index 00000000..49f04748 --- /dev/null +++ b/picos_std/_doc-dir/CHANGES.md @@ -0,0 +1,171 @@ +## 0.5.0 + +- Major additions, changes, bug fixes, improvements, and restructuring + (@polytypic, @c-cube) + + - Additions: + + - Minimalistic Cohttp implementation + - Implicitly propagated `Flock` of fibers for structured concurrency + - Option to terminate `Bundle` and `Flock` on return + - `Event` abstraction + - Synchronization and communication primitives: + - Incremental variable or `Ivar` + - Countdown `Latch` + - `Semaphore` + - `Stream` of events + - Multi-producer, multi-consumer lock-free queue optimized for schedulers + - Multithreaded (work-stealing) FIFO scheduler + - Support `quota` for FIFO based schedulers + - Transactional interface for atomically completing multiple `Computation`s + + - Changes: + + - Redesigned resource management based on `('r -> 'a) -> 'a` functions + - Redesigned `spawn` interface allowing `FLS` entries to be populated before + spawn + - Introduced concept of fatal errors, which must terminate the scheduler or + the whole program + - Simplified `FLS` interface + - Removed `Exn_bt` + + - Improvements: + + - Signficantly reduced per fiber memory usage of various sample schedulers + + - Picos has now been split into multiple packages and libraries: + + - pkg: `picos` + - lib: `picos` + - lib: `picos.domain` + - lib: `picos.thread` + - pkg: `picos_aux` + - lib: `picos_aux.htbl` + - lib: `picos_aux.mpmcq` + - lib: `picos_aux.mpscq` + - lib: `picos_aux.rc` + - pkg: `picos_lwt` + - lib: `picos_lwt` + - lib: `picos_lwt.unix` + - pkg: `picos_meta` (integration tests) + - pkg: `picos_mux` + - lib: `picos_mux.fifo` + - lib: `picos_mux.multififo` + - lib: `picos_mux.random` + - lib: `picos_mux.thread` + - pkg: `picos_std` + - lib: `picos_std.event` + - lib: `picos_std.finally` + - lib: `picos_std.structured` + - lib: `picos_std.sync` + - pkg: `picos_io` + - lib: `picos_io` + - lib: `picos_io.fd` + - lib: `picos_io.select` + - pkg: `picos_io_cohttp` + - lib: `picos_io_cohttp` + +## 0.4.0 + +- Renamed `Picos_mpsc_queue` to `Picos_mpscq`. (@polytypic) + +- Core API changes: + + - Added `Computation.returned`. (@polytypic) + +- `Lwt` interop improvements: + + - Fixed `Picos_lwt` handling of `Cancel_after` to not raise in case of + cancelation. (@polytypic) + + - Redesigned `Picos_lwt` to take a `System` module, which must implement a + semi thread-safe trigger mechanism to allow unblocking `Lwt` promises on the + main thread. (@polytypic) + + - Added `Picos_lwt_unix` interface to `Lwt`, which includes an internal + `System` module implemented using `Lwt_unix`. (@polytypic) + + - Dropped thunking from `Picos_lwt.await`. (@polytypic) + +- Added a randomized multicore scheduler `Picos_randos` for testing. + (@polytypic) + +- Changed `Picos_select.check_configured` to always (re)configure signal + handling on the current thread. (@polytypic) + +- `Picos_structured`: + + - Added a minimalistic `Promise` abstraction. (@polytypic) + - Changed to more consistently not treat `Terminate` as an error. (@polytypic) + +- Changed schedulers to take `~forbid` as an optional argument. (@polytypic) + +- Various minor additions, fixes, and documentation improvements. (@polytypic) + +## 0.3.0 + +- Core API changes: + + - Added `Fiber.set_computation`, which represents a semantic change + - Renamed `Fiber.computation` to `Fiber.get_computation` + - Added `Computation.attach_canceler` + - Added `Fiber.sleep` + - Added `Fiber.create_packed` + - Removed `Fiber.try_attach` + - Removed `Fiber.detach` + + Most of the above changes were motivated by work on and requirements of the + added structured concurrency library (@polytypic) + +- Added a basic user level structured concurrent programming library + `Picos_structured` (@polytypic) + +- Added a functorized `Picos_lwt` providing direct style effects based interface + to programming with Lwt (@polytypic) + +- Added missing `Picos_stdio.Unix.select` (@polytypic) + +## 0.2.0 + +- Documentation fixes and restructuring (@polytypic) +- Scheduler friendly `waitpid`, `wait`, and `system` in `Picos_stdio.Unix` for + platforms other than Windows (@polytypic) +- Added `Picos_select.configure` to allow, and sometimes require, configuring + `Picos_select` for co-operation with libraries that also deal with signals + (@polytypic) +- Moved `Picos_tls` into `Picos_thread.TLS` (@polytypic) +- Enhanced `sleep` and `sleepf` in `Picos_stdio.Unix` to block in a scheduler + friendly manner (@polytypic) + +## 0.1.0 + +- First experimental release of Picos. + + Core: + + - `picos` — A framework for interoperable effects based concurrency. + + Sample schedulers: + + - `picos.fifos` — Basic single-threaded effects based Picos compatible + scheduler for OCaml 5. + - `picos.threaded` — Basic `Thread` based Picos compatible scheduler for + OCaml 4. + + Scheduler agnostic libraries: + + - `picos.sync` — Basic communication and synchronization primitives for Picos. + - `picos.stdio` — Basic IO facilities based on OCaml standard libraries for + Picos. + - `picos.select` — Basic `Unix.select` based IO event loop for Picos. + + Auxiliary libraries: + + - `picos.domain` — Minimalistic domain API available both on OCaml 5 and on + OCaml 4. + - `picos.exn_bt` — Wrapper for exceptions with backtraces. + - `picos.fd` — Externally reference counted file descriptors. + - `picos.htbl` — Lock-free hash table. + - `picos.mpsc_queue` — Multi-producer, single-consumer queue. + - `picos.rc` — External reference counting tables for disposable resources. + - `picos.tls` — Thread-local storage. diff --git a/picos_std/_doc-dir/LICENSE.md b/picos_std/_doc-dir/LICENSE.md new file mode 100644 index 00000000..5da69623 --- /dev/null +++ b/picos_std/_doc-dir/LICENSE.md @@ -0,0 +1,13 @@ +Copyright © 2023 Vesa Karvonen + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/picos_std/_doc-dir/README.md b/picos_std/_doc-dir/README.md new file mode 100644 index 00000000..26e31b8e --- /dev/null +++ b/picos_std/_doc-dir/README.md @@ -0,0 +1,791 @@ +[API reference](https://ocaml-multicore.github.io/picos/doc/index.html) · +[Benchmarks](https://bench.ci.dev/ocaml-multicore/picos/branch/main?worker=pascal&image=bench.Dockerfile) +· +[Stdlib Benchmarks](https://bench.ci.dev/ocaml-multicore/multicore-bench/branch/main?worker=pascal&image=bench.Dockerfile) + +# **Picos** — Interoperable effects based concurrency + +Picos is a +[systems programming](https://en.wikipedia.org/wiki/Systems_programming) +interface between effects based schedulers and concurrent abstractions. + +

+ +Picos is designed to enable an _open ecosystem_ of +[interoperable](https://en.wikipedia.org/wiki/Interoperability) and +interchangeable elements of effects based cooperative concurrent programming +models such as + +- [schedulers]() that + multiplex large numbers of + [user level fibers](https://en.wikipedia.org/wiki/Green_thread) to run on a + small number of system level threads, +- mechanisms for managing fibers and for + [structuring concurrency](https://en.wikipedia.org/wiki/Structured_concurrency), +- communication and synchronization primitives, such as + [mutexes and condition variables](), + message queues, + [STM](https://en.wikipedia.org/wiki/Software_transactional_memory)s, and more, + and +- integrations with low level + [asynchronous IO](https://en.wikipedia.org/wiki/Asynchronous_I/O) systems + +by decoupling such elements from each other. + +Picos comes with a +[reference manual](https://ocaml-multicore.github.io/picos/doc/index.html) and +many sample libraries. + +⚠️ Please note that Picos is still considered experimental and unstable. + +## Introduction + +Picos addresses the incompatibility of effects based schedulers at a fundamental +level by introducing +[an _interface_ to decouple schedulers and other concurrent abstractions](https://ocaml-multicore.github.io/picos/doc/picos/Picos/index.html) +that need services from a scheduler. + +The +[core abstractions of Picos](https://ocaml-multicore.github.io/picos/doc/picos/Picos/index.html#the-architecture-of-picos) +are + +- [`Trigger`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Trigger/index.html) + — the ability to await for a signal, +- [`Computation`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Computation/index.html) + — a cancelable computation, and +- [`Fiber`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Fiber/index.html) + — an independent thread of execution, + +that are implemented partially by the Picos interface in terms of the effects + +- [`Trigger.Await`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Trigger/index.html#extension-Await) + — to suspend and resume a fiber, +- [`Computation.Cancel_after`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Computation/index.html#extension-Cancel_after) + — to cancel a computation after given period of time, +- [`Fiber.Current`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Fiber/index.html#extension-Current) + — to obtain the current fiber, +- [`Fiber.Yield`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Fiber/index.html#extension-Yield) + — to request rescheduling, and +- [`Fiber.Spawn`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Fiber/index.html#extension-Spawn) + — to start a new fiber. + +The partial implementation of the abstractions and the effects define a contract +between schedulers and other concurrent abstractions. By handling the Picos +effects according to the contract a scheduler becomes _Picos compatible_, which +allows any abstractions written against the Picos interface, i.e. _Implemented +in Picos_, to be used with the scheduler. + +### Understanding cancelation + +A central idea or goal of Picos is to provide a collection of building blocks +for parallelism safe cancelation that allows the implementation of both blocking +abstractions as well as the implementation of abstractions for structuring +fibers for cancelation or managing the propagation and scope of cancelation. + +While cancelation, which is essentially a kind of asynchronous exception or +signal, is not necessarily recommended as a general control mechanism, the +ability to cancel fibers in case of errors is crucial for the implementation of +practical concurrent programming models. + +Consider the following characteristic +[example](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_structured/index.html#understanding-cancelation): + +```ocaml skip +Mutex.protect mutex begin fun () -> + while true do + Condition.wait condition mutex + done +end +``` + +Assume that a fiber executing the above code might be canceled, at any point, by +another fiber running in parallel. This could be necessary, for example, due to +an error that requires the application to be shut down. How could that be done +while ensuring both +[safety and liveness](https://en.wikipedia.org/wiki/Safety_and_liveness_properties)? + +- For safety, cancelation should not leave the program in an invalid state or + cause the program to leak memory. In this case, `Condition.wait` must exit + with the mutex locked, even in case of cancelation, and, as `Mutex.protect` + exits, the ownership of the mutex must be transferred to the next fiber, if + any, waiting in queue for the mutex. No references to unused objects may be + left in the mutex or the condition variable. + +- For liveness, cancelation should ensure that the fiber will eventually + continue after cancelation. In this case, cancelation could be triggered + during the `Mutex.lock` operation inside `Mutex.protect` or the + `Condition.wait` operation, when the fiber might be in a suspended state, and + cancelation should then allow the fiber to continue. + +The set of abstractions, `Trigger`, `Computation`, and `Fiber`, work together +[to support cancelation](https://ocaml-multicore.github.io/picos/doc/picos/Picos/index.html#cancelation-in-picos). +Briefly, a fiber corresponds to an independent thread of execution and every +fiber is associated with a computation at all times. When a fiber creates a +trigger in order to await for a signal, it ask the scheduler to suspend the +fiber on the trigger. Assuming the fiber has not forbidden the propagation of +cancelation, which is required, for example, in the implementation of +`Condition.wait` to lock the mutex upon exit, the scheduler must also attach the +trigger to the computation associated with the fiber. If the computation is then +canceled before the trigger is otherwise signaled, the trigger will be signaled +by the cancelation of the computation, and the fiber will be resumed by the +scheduler as canceled. + +This cancelable suspension protocol and its partial implementation designed +around the first-order +[`Trigger.Await`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Trigger/index.html#extension-Await) +effect creates a clear separation between schedulers and user code running in +fibers and is designed to handle the possibility of a trigger being signaled or +a computation being canceled at any point during the suspension of a fiber. +Schedulers are given maximal freedom to decide which fiber to resume next. As an +example, a scheduler could give priority to canceled fibers — going as far +as moving a fiber already in the ready queue of the scheduler to the front of +the queue at the point of cancelation — based on the assumption that user +code promptly cancels external requests and frees critical resources. + +### `Trigger` + +A trigger provides the ability to await for a signal and is perhaps the best +established and least controversial element of the Picos interface. + +Here is an extract from the signature of the +[`Trigger` module](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Trigger/index.html): + + + +```ocaml skip +type t +val create : unit -> t +val await : t -> (exn * Printexc.raw_backtrace) option +val signal : t -> unit +val on_signal : (* for schedulers *) +``` + +The idea is that a fiber may create a trigger, insert it into some shared data +structure, and then call `await` to ask the scheduler to suspend the fiber until +something signals the trigger. When `await` returns an exception with a +backtrace it means that the fiber has been canceled. + +As an example, let's consider the implementation of an `Ivar` or incremental or +single-assignment variable: + +```ocaml skip +type 'a t +val create : unit -> 'a t +val try_fill : 'a t -> 'a -> bool +val read : 'a t -> 'a +``` + +An `Ivar` is created as empty and can be filled with a value once. An attempt to +read an `Ivar` blocks until the `Ivar` is filled. + +Using `Trigger` and `Atomic`, we can represent an `Ivar` as follows: + +```ocaml +type 'a state = + | Filled of 'a + | Empty of Trigger.t list + +type 'a t = 'a state Atomic.t +``` + +The `try_fill` operation is then fairly straightforward to implement: + +```ocaml +let rec try_fill t value = + match Atomic.get t with + | Filled _ -> false + | Empty triggers as before -> + let after = Filled value in + if Atomic.compare_and_set t before after then + begin + List.iter Trigger.signal triggers; (* ! *) + true + end + else + try_fill t value +``` + +The interesting detail above is that after successfully filling an `Ivar`, the +triggers are signaled. This allows the `await` inside the `read` operation to +return: + + + +```ocaml +let rec read t = + match Atomic.get t with + | Filled value -> value + | Empty triggers as before -> + let trigger = Trigger.create () in + let after = Empty (trigger :: triggers) in + if Atomic.compare_and_set t before after then + match Trigger.await trigger with + | None -> read t + | Some (exn, bt) -> + cleanup t trigger; (* ! *) + Printexc.raise_with_backtrace exn bt + else + read t +``` + +An important detail above is that when `await` returns an exception with a +backtrace, meaning that the fiber has been canceled, the `cleanup` operation +(which is omitted) is called to remove the `trigger` from the `Ivar` to avoid +potentially accumulating unbounded numbers of triggers in an empty `Ivar`. + +As simple as it is, the design of `Trigger` is far from arbitrary: + +- First of all, `Trigger` has single-assignment semantics. After being signaled, + a trigger takes a constant amount of space and does not point to any other + heap object. This makes it easier to reason about the behavior and can also + help to avoid leaks or optimize data structures containing triggers, because + it is safe to hold bounded amounts of signaled triggers. + +- The `Trigger` abstraction is essentially first-order, which provides a clear + separation between a scheduler and programs, or fibers, running on a + scheduler. The `await` operation performs the `Await` effect, which passes the + trigger to the scheduler. The scheduler then attaches its own callback to the + trigger using `on_signal`. This way a scheduler does not call arbitrary user + specified code in the `Await` effect handler. + +- Separating the creation of a trigger from the `await` operation allows one to + easily insert a trigger into any number of places and allows the trigger to be + potentially concurrently signaled before the `Await` effect is performed in + which case the effect can be skipped entirely. + +- No value is propagated with a trigger. This makes triggers simpler and makes + it less likely for one to e.g. accidentally drop such a value. In many cases, + like with the `Ivar`, there is already a data structure through which values + can be propagated. + +- The `signal` operation gives no indication of whether a fiber will then be + resumed as canceled or not. This gives maximal flexibility for the scheduler + and also makes it clear that cancelation must be handled based on the return + value of `await`. + +### `Computation` + +A `Computation` basically holds the status, i.e. _running_, _returned_, or +_canceled_, of some sort of computation and allows anyone with access to the +computation to attach triggers to it to be signaled in case the computation +stops running. + +Here is an extract from the signature of the +[`Computation` module](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Computation/index.html): + +```ocaml skip +type 'a t + +val create : unit -> 'a t + +val try_attach : 'a t -> Trigger.t -> bool +val detach : 'a t -> Trigger.t -> unit + +val try_return : 'a t -> 'a -> bool +val try_cancel : 'a t -> exn -> Printexc.raw_backtrace -> bool + +val check : 'a t -> unit +val await : 'a t -> 'a +``` + +A `Computation` directly provides a superset of the functionality of the `Ivar` +we sketched in the previous section: + +```ocaml +type 'a t = 'a Computation.t +let create : unit -> 'a t = Computation.create +let try_fill : 'a t -> 'a -> bool = + Computation.try_return +let read : 'a t -> 'a = Computation.await +``` + +However, what really makes the `Computation` useful is the ability to +momentarily attach triggers to it. A `Computation` essentially implements a +specialized lock-free bag of triggers, which allows one to implement dynamic +completion propagation networks. + +The `Computation` abstraction is also designed with both simplicity and +flexibility in mind: + +- Similarly to `Trigger`, `Computation` has single-assignment semantics, which + makes it easier to reason about. + +- Unlike a typical cancelation context of a structured concurrency model, + `Computation` is unopinionated in that it does not impose a specific + hierarchical structure. + +- Anyone may ask to be notified when a `Computation` is completed by attaching + triggers to it and anyone may complete a `Computation`. This makes + `Computation` an omnidirectional communication primitive. + +Interestingly, and unintentionally, it turns out that, given +[the ability to complete two (or more) computations atomically](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Computation/Tx/index.html), +`Computation` is essentially expressive enough to implement the +[event](https://ocaml.org/manual/latest/api/Event.html) abstraction of +[Concurrent ML](https://en.wikipedia.org/wiki/Concurrent_ML). The same features +that make `Computation` suitable for implementing more or less arbitrary dynamic +completion propagation networks make it suitable for implementing Concurrent ML +style abstractions. + +### `Fiber` + +A fiber corresponds to an independent thread of execution. Technically an +effects based scheduler creates a fiber, effectively giving it an identity, as +it runs some function under its handler. The `Fiber` abstraction provides a way +to share a proxy identity, and a bit of state, between a scheduler and other +concurrent abstractions. + +Here is an extract from the signature of the +[`Fiber` module](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Fiber/index.html): + +```ocaml skip +type t + +val current : unit -> t + +val create : forbid:bool -> 'a Computation.t -> t +val spawn : t -> (t -> unit) -> unit + +val get_computation : t -> Computation.packed +val set_computation : t -> Computation.packed -> unit + +val has_forbidden : t -> bool +val exchange : t -> forbid:bool -> bool + +module FLS : sig (* ... *) end +``` + +Fibers are where all of the low level bits and pieces of Picos come together, +which makes it difficult to give both meaningful and concise examples, but let's +implement a slightly simplistic structured concurrency mechanism: + +```ocaml skip +type t (* represents a scope *) +val run : (t -> unit) -> unit +val fork : t -> (unit -> unit) -> unit +``` + +The idea here is that `run` creates a "scope" and waits until all of the fibers +forked into the scope have finished. In case any fiber raises an unhandled +exception, or the main fiber that created the scope is canceled, all of the +fibers are canceled and an exception is raised. To keep things slightly simpler, +only the first exception is kept. + +A scope can be represented by a simple record type: + +```ocaml +type t = { + count : int Atomic.t; + inner : unit Computation.t; + ended : Trigger.t; +} +``` + +The idea is that after a fiber is finished, we decrement the count and if it +becomes zero, we finish the computation and signal the main fiber that the scope +has ended: + +```ocaml +let decr t = + let n = Atomic.fetch_and_add t.count (-1) in + if n = 1 then begin + Computation.finish t.inner; + Trigger.signal t.ended + end +``` + +When forking a fiber, we increment the count unless it already was zero, in +which case we raise an error: + +```ocaml +let rec incr t = + let n = Atomic.get t.count in + if n = 0 then invalid_arg "ended"; + if not (Atomic.compare_and_set t.count n (n + 1)) + then incr t +``` + +The fork operation is now relatively straightforward to implement: + +```ocaml +let fork t action = + incr t; + try + let main _ = + match action () with + | () -> decr t + | exception exn -> + let bt = Printexc.get_raw_backtrace () in + Computation.cancel t.inner exn bt; + decr t + in + let fiber = + Fiber.create ~forbid:false t.inner + in + Fiber.spawn fiber main + with canceled_exn -> + decr t; + raise canceled_exn +``` + +The above `fork` first increments the count and then tries to spawn a fiber. The +Picos interface specifies that when `Fiber.spawn` returns normally, the action, +`main`, must be called by the scheduler. This allows us to ensure that the +increment is always matched with a decrement. + +Setting up a scope is the most complex operation: + + + +```ocaml +let run body = + let count = Atomic.make 1 in + let inner = Computation.create () in + let ended = Trigger.create () in + let t = { count; inner; ended } in + let fiber = Fiber.current () in + let (Packed outer) = + Fiber.get_computation fiber + in + let canceler = + Computation.attach_canceler + ~from:outer + ~into:t.inner + in + match + Fiber.set_computation fiber (Packed t.inner); + body t + with + | () -> join t outer canceler fiber + | exception exn -> + let bt = Printexc.get_raw_backtrace () in + Computation.cancel t.inner exn bt; + join t outer canceler fiber; + Printexc.raise_with_backtrace exn bt +``` + +The `Computation.attach_canceler` operation attaches a special trigger to +propagate cancelation from one computation into another. After the body exits, +`join` + +```ocaml +let join t outer canceler fiber = + decr t; + Fiber.set_computation fiber (Packed outer); + let forbid = Fiber.exchange fiber ~forbid:true in + Trigger.await t.ended |> ignore; + Fiber.set fiber ~forbid; + Computation.detach outer canceler; + Computation.check t.inner; + Fiber.check fiber +``` + +is called to wait for the scoped fibers and restore the state of the main fiber. +An important detail is that propagation of cancelation is forbidden by setting +the `forbid` flag to `true` before the call of `Trigger.await`. This is +necessary to ensure that `join` does not exit, due to the fiber being canceled, +before all of the child fibers have actually finished. Finally, `join` checks +the inner computation and the fiber, which means that an exception will be +raised in case either was canceled. + +The design of `Fiber` includes several key features: + +- The low level design allows one to both avoid unnecessary overheads, such as + allocating a `Computation.t` for every fiber, when implementing simple + abstractions and also to implement more complex behaviors that might prove + difficult given e.g. a higher level design with a built-in notion of + hierarchy. + +- As `Fiber.t` stores the `forbid` flag and the `Computation.t` associated with + the fiber one need not pass those as arguments through the program. This + allows various concurrent abstractions to be given traditional interfaces, + which would otherwise need to be complicated. + +- Effects are relatively expensive. The cost of performing effects can be + amortized by obtaining the `Fiber.t` once and then manipulating it multiple + times. + +- A `Fiber.t` also provides an identity for the fiber. It has so far proven to + be sufficient for most purposes. Fiber local storage, which we do not cover + here, can be used to implement, for example, a unique integer id for fibers. + +### Assumptions + +Now, consider the `Ivar` abstraction presented earlier as an example of the use +of the `Trigger` abstraction. That `Ivar` implementation, as well as the +`Computation` based implementation, works exactly as desired inside the scope +abstraction presented in the previous section. In particular, a blocked +`Ivar.read` can be canceled, either when another fiber in a scope raises an +unhandled exception or when the main fiber of the scope is canceled, which +allows the fiber to continue by raising an exception after cleaning up. In fact, +Picos comes with a number of libraries that all would work quite nicely with the +examples presented here. + +For example, a library provides an operation to run a block with a timeout on +the current fiber. One could use it with `Ivar.read` to implement a read +operation +[with a timeout](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_structured/Control/index.html#val-terminate_after): + +```ocaml +let read_in ~seconds ivar = + Control.terminate_after ~seconds @@ fun () -> + Ivar.read ivar +``` + +This interoperability is not accidental. For example, the scope abstraction +basically assumes that one does not use `Fiber.set_computation`, in an arbitrary +unscoped manner inside the scoped fibers. An idea with the Picos interface +actually is that it is not supposed to be used by applications at all and most +higher level libraries should be built on top of libraries that do not directly +expose elements of the Picos interface. + +Perhaps more interestingly, there are obviously limits to what can be achieved +in an "interoperable" manner. Imagine an operation like + +```ocaml skip +val at_exit : (unit -> unit) -> unit +``` + +that would allow one to run an action just before a fiber exits. One could, of +course, use a custom spawn function that would support such cleanup, but then +`at_exit` could only be used on fibers spawned through that particular spawn +function. + +### The effects + +As mentioned previously, the Picos interface is implemented partially in terms +of five effects: + +```ocaml version>=5.0.0 +type _ Effect.t += + | Await : Trigger.t -> (exn * Printexc.raw_backtrace) option Effect.t + | Cancel_after : { + seconds : float; + exn: exn; + bt : Printexc.raw_backtrace; + computation : 'a Computation.t; + } + -> unit Effect.t + | Current : t Effect.t + | Yield : unit Effect.t + | Spawn : { + fiber : Fiber.t; + main : (Fiber.t -> unit); + } + -> unit Effect.t +``` + +A scheduler must handle those effects as specified in the Picos documentation. + +The Picos interface does not, in particular, dictate which ready fibers a +scheduler must run next and on which domains. Picos also does not require that a +fiber should stay on the domain on which it was spawned. Abstractions +implemented against the Picos interface should not assume any particular +scheduling. + +Picos actually comes with +[a randomized multithreaded scheduler](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_randos/index.html), +that, after handling any of the effects, picks the next ready fiber randomly. It +has proven to be useful for testing that abstractions implemented in Picos do +not make invalid scheduling assumptions. + +When a concurrent abstraction requires a particular scheduling, it should +primarily be achieved through the use of synchronization abstractions like when +programming with traditional threads. Application programs may, of course, pick +specific schedulers. + +## Status and results + +We have an experimental design and implementation of the core Picos interface as +illustrated in the previous section. We have also created several _Picos +compatible_ +[sample schedulers](https://ocaml-multicore.github.io/picos/doc/picos_mux/index.html). +A scheduler, in this context, just multiplexes fibers to run on one or more +system level threads. We have also created some sample higher-level +[scheduler agnostic libraries](https://ocaml-multicore.github.io/picos/doc/picos_std/index.html) +_Implemented in Picos_. These libraries include +[a library for resource management](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_finally/index.html), +[a library for structured concurrency](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_structured/index.html), +[a library of synchronization primitives](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_sync/index.html), +and +[an asynchronous I/O library](https://ocaml-multicore.github.io/picos/doc/picos_io/Picos_io/index.html). +The synchronization library and the I/O library intentionally mimic libraries +that come with the OCaml distribution. All of the libraries work with all of the +schedulers and all of these _elements_ are interoperable and entirely opt-in. + +What is worth explicitly noting is that all of these schedulers and libraries +are small, independent, and highly modular pieces of code. They all crucially +depend on and are decoupled from each other via the core Picos interface +library. A basic single threaded scheduler implementation requires only about +100 lines of code (LOC). A more complex parallel scheduler might require a +couple of hundred LOC. The scheduler agnostic libraries are similarly small. + +Here is an +[example](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_structured/index.html#a-simple-echo-server-and-clients) +of a concurrent echo server using the scheduler agnostic libraries provided as +samples: + +```ocaml +let run_server server_fd = + Unix.listen server_fd 8; + Flock.join_after begin fun () -> + while true do + let@ client_fd = instantiate Unix.close @@ fun () -> + Unix.accept ~cloexec:true server_fd |> fst + in + Flock.fork begin fun () -> + let@ client_fd = move client_fd in + Unix.set_nonblock client_fd; + let bs = Bytes.create 100 in + let n = + Unix.read client_fd bs 0 (Bytes.length bs) + in + Unix.write client_fd bs 0 n |> ignore + end + done + end +``` + +The +[`Unix`](https://ocaml-multicore.github.io/picos/doc/picos_io/Picos_io/Unix/index.html) +module is provided by the I/O library. The operations on file descriptors on +that module, such as `accept`, `read`, and `write`, use the Picos interface to +suspend fibers allowing other fibers to run while waiting for I/O. The +[`Flock`](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_structured/Flock/index.html) +module comes from the structured concurrency library. A call of +[`join_after`](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_structured/Flock/index.html#val-join_after) +returns only after all the fibers +[`fork`](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_structured/Flock/index.html#val-fork)ed +into the flock have terminated. If the main fiber of the flock is canceled, or +any fiber within the flock raises an unhandled exception, all the fibers within +the flock will be canceled and an exception will be raised on the main fiber of +the flock. The +[`let@`](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_finally/index.html#val-let@), +[`finally`](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_finally/index.html#val-instantiate), +and +[`move`](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_finally/index.html#val-move) +operations come from the resource management library and allow dealing with +resources in a leak-free manner. The responsibility to close the `client_fd` +socket is +[`move`](https://ocaml-multicore.github.io/picos/doc/picos_std/Picos_std_finally/index.html#val-move)d +from the main server fiber to a fiber forked to handle that client. + +We should emphasize that the above is just an example. The Picos interface +should be both expressive and efficient enough to support practical +implementations of many different kinds of concurrent programming models. Also, +as described previously, the Picos interface does not, for example, internally +implement structured concurrency. However, the abstractions provided by Picos +are designed to allow structured and unstructured concurrency to be _Implemented +in Picos_ as libraries that will then work with any _Picos compatible_ scheduler +and with other concurrent abstractions. + +Finally, an interesting demonstration that Picos really fundamentally is an +interface is +[a prototype _Picos compatible_ direct style interface to Lwt](https://ocaml-multicore.github.io/picos/doc/picos_lwt/Picos_lwt/index.html). +The implementation uses shallow effect handlers and defers all scheduling +decisions to Lwt. Running a program with the scheduler returns a Lwt promise. + +## Future work + +As mentioned previously, Picos is still an ongoing project and the design is +considered experimental. We hope that Picos soon matures to serve the needs of +both the commercial users of OCaml and the community at large. + +Previous sections already touched a couple of updates currently in development, +such as the support for finalizing resources stored in +[`FLS`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Fiber/FLS/index.html) +and the development of Concurrent ML style abstractions. We also have ongoing +work to formalize aspects of the Picos interface. + +One potential change we will be investigating is whether the +[`Computation`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Computation/index.html) +abstraction should be simplified to only support cancelation. + +The implementation of some operations, such as +[`Fiber.current`](https://ocaml-multicore.github.io/picos/doc/picos/Picos/Fiber/index.html#val-current) +to retrieve the current fiber proxy identity, do not strictly need to be +effects. Performing an effect is relatively expensive and we will likely design +a mechanism to store a reference to the current fiber in some sort of local +storage, which could significantly improve the performance of certain +abstractions, such as checked mutexes, that need to access the current fiber. + +We also plan to develop a minimalist library for spawning threads over domains, +much like Moonpool, in a cooperative manner for schedulers and other libraries. + +We also plan to make Domainslib Picos compatible, which will require developing +a more efficient non-effects based interface for spawning fibers, and +investigate making Eio Picos compatible. + +We also plan to design and implement asynchronous IO libraries for Picos using +various system call interface for asynchronous IO such as io_uring. + +Finally, Picos is supposed to be an _open ecosystem_. If you have feedback or +would like to work on something mentioned above, let us know. + +## Motivation + +There are already several concrete effects-based concurrent programming +libraries and models being developed. Here is a list of some such publicly +available projects:[\*](https://xkcd.com/927/) + +1. [Affect](https://github.com/dbuenzli/affect) — "Composable concurrency + primitives with OCaml effects handlers (unreleased)", +2. [Domainslib](https://github.com/ocaml-multicore/domainslib) — + "Nested-parallel programming", +3. [Eio](https://github.com/ocaml-multicore/eio) — "Effects-Based Parallel IO + for OCaml", +4. [Fuseau](https://github.com/c-cube/fuseau) — "Lightweight fiber library for + OCaml 5", +5. [Miou](https://github.com/robur-coop/miou) — "A simple scheduler for OCaml + 5", +6. [Moonpool](https://github.com/c-cube/moonpool) — "Commodity thread pools for + OCaml 5", and +7. [Riot](https://github.com/leostera/riot) — "An actor-model multi-core + scheduler for OCaml 5". + +All of the above libraries are mutually incompatible with each other with the +exception that Domainslib, Eio, and Moonpool implement an earlier +interoperability proposal called +[domain-local-await](https://github.com/ocaml-multicore/domain-local-await/) or +DLA, which allows a concurrent programming library like +[Kcas](https://github.com/ocaml-multicore/kcas/)[\*](https://github.com/ocaml-multicore/kcas/pull/136) +to work on all of those. Unfortunately, DLA, by itself, is known to be +insufficient and the design has not been universally accepted. + +By introducing a scheduler interface and key libraries, such as an IO library, +implemented on top of the interface, we hope that the scarce resources of the +OCaml community are not further divided into mutually incompatible ecosystems +built on top of such mutually incompatible concurrent programming libraries, +while, simultaneously, making it possible to experiment with many kinds of +concurrent programming models. + +It should be +technically[\*](https://www.youtube.com/watch?v=hou0lU8WMgo) possible +for all the previously mentioned libraries, except +[Miou](https://github.com/robur-coop/miou), to + +1. be made + [Picos compatible](https://ocaml-multicore.github.io/picos/doc/picos/index.html#picos-compatible), + i.e. to handle the Picos effects, and +2. have their elements + [implemented in Picos](https://ocaml-multicore.github.io/picos/doc/picos/index.html#implemented-in-picos), + i.e. to make them usable on other Picos-compatible schedulers. + +Please read +[the reference manual](https://ocaml-multicore.github.io/picos/doc/index.html) +for further information. diff --git a/picos_std/_doc-dir/odoc-pages/index.mld b/picos_std/_doc-dir/odoc-pages/index.mld new file mode 100644 index 00000000..287c8ca5 --- /dev/null +++ b/picos_std/_doc-dir/odoc-pages/index.mld @@ -0,0 +1,17 @@ +{0 Sample libraries for Picos} + +This package contains sample scheduler agnostic libraries for {!Picos}. Many of +the modules are intentionally designed to mimic modules from the OCaml Stdlib. + +{!modules: + Picos_std_finally + Picos_std_event + Picos_std_structured + Picos_std_sync +} + +{^ These libraries are both meant to serve as examples of what can be done and + to also provide practical means for programming with fibers. Hopefully there + will be many more libraries implemented in Picos like these providing + different approaches, patterns, and idioms for structuring concurrent + programs.} diff --git a/picos_std/index.html b/picos_std/index.html new file mode 100644 index 00000000..9e9f9264 --- /dev/null +++ b/picos_std/index.html @@ -0,0 +1,2 @@ + +index (picos_std.index)

Package picos_std

This package contains sample scheduler agnostic libraries for Picos. Many of the modules are intentionally designed to mimic modules from the OCaml Stdlib.

These libraries are both meant to serve as examples of what can be done and to also provide practical means for programming with fibers. Hopefully there will be many more libraries implemented in Picos like these providing different approaches, patterns, and idioms for structuring concurrent programs.

Package info

changes-files
license-files
readme-files
diff --git a/thread-local-storage/Thread_local_storage/index.html b/thread-local-storage/Thread_local_storage/index.html new file mode 100644 index 00000000..b211bd7f --- /dev/null +++ b/thread-local-storage/Thread_local_storage/index.html @@ -0,0 +1,2 @@ + +Thread_local_storage (thread-local-storage.Thread_local_storage)

Module Thread_local_storage

Thread local storage

type 'a t

A TLS slot for values of type 'a. This allows the storage of a single value of type 'a per thread.

val create : unit -> 'a t

Allocate a new TLS slot. The TLS slot starts uninitialised on each thread.

Note: each TLS slot allocated consumes a word per thread, and it can never be deallocated. create should be called at toplevel to produce constants, do not use it in a loop.

  • raises Failure

    if no more TLS slots can be allocated.

exception Not_set

Exception raised when accessing a slot that was not previously set on this thread

val get_exn : 'a t -> 'a

get_exn x returns the value previously stored in the TLS slot x for the current thread.

This function is safe to use from asynchronous callbacks without risks of races.

  • raises Not_set

    if the TLS slot has not been initialised in the current thread. Do note that this uses raise_notrace for performance reasons, so make sure to always catch the exception as it will not carry a backtrace.

val get_opt : 'a t -> 'a option

get_opt x returns Some v where v is the value previously stored in the TLS slot x for the current thread. It returns None if the TLS slot has not been initialised in the current thread.

This function is safe to use from asynchronous callbacks without risks of races.

val get_default : default:(unit -> 'a) -> 'a t -> 'a

get_default x ~default returns the value previously stored in the TLS slot x for the current thread. If the TLS slot has not been initialised, default () is called, and its result is returned instead, after being stored in the TLS slot x.

If the call to default () raises an exception, then the TLS slot remains uninitialised. If default is a function that always raises, then this function is safe to use from asynchronous callbacks without risks of races.

  • raises Out_of_memory
val set : 'a t -> 'a -> unit

set x v stores v into the TLS slot x for the current thread.

  • raises Out_of_memory
diff --git a/thread-local-storage/Thread_local_storage__/index.html b/thread-local-storage/Thread_local_storage__/index.html new file mode 100644 index 00000000..2c724528 --- /dev/null +++ b/thread-local-storage/Thread_local_storage__/index.html @@ -0,0 +1,2 @@ + +Thread_local_storage__ (thread-local-storage.Thread_local_storage__)

Module Thread_local_storage__

This module is hidden.

diff --git a/thread-local-storage/Thread_local_storage__Atomic/index.html b/thread-local-storage/Thread_local_storage__Atomic/index.html new file mode 100644 index 00000000..aefd1bf5 --- /dev/null +++ b/thread-local-storage/Thread_local_storage__Atomic/index.html @@ -0,0 +1,2 @@ + +Thread_local_storage__Atomic (thread-local-storage.Thread_local_storage__Atomic)

Module Thread_local_storage__Atomic

This module is hidden.

diff --git a/thread-local-storage/_doc-dir/CHANGES.md b/thread-local-storage/_doc-dir/CHANGES.md new file mode 100644 index 00000000..91e239a0 --- /dev/null +++ b/thread-local-storage/_doc-dir/CHANGES.md @@ -0,0 +1,11 @@ + +# 0.2 + +- Incompatible API change, making things simpler. +- It is now safe to read TLS from asynchronous callbacks (e.g. memprof + callbacks). + +# 0.1 + +initial release + diff --git a/thread-local-storage/_doc-dir/LICENSE b/thread-local-storage/_doc-dir/LICENSE new file mode 100644 index 00000000..11be746c --- /dev/null +++ b/thread-local-storage/_doc-dir/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Simon Cruanes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/thread-local-storage/_doc-dir/README.md b/thread-local-storage/_doc-dir/README.md new file mode 100644 index 00000000..ff9ad245 --- /dev/null +++ b/thread-local-storage/_doc-dir/README.md @@ -0,0 +1,10 @@ +# Thread-local storage + +The classic threading utility: have variables that have a value for each thread. + +See https://discuss.ocaml.org/t/a-hack-to-implement-efficient-tls-thread-local-storage/13264 for the initial implementation +by @polytypic. + +## License + +MIT diff --git a/thread-local-storage/index.html b/thread-local-storage/index.html new file mode 100644 index 00000000..90a5db83 --- /dev/null +++ b/thread-local-storage/index.html @@ -0,0 +1,2 @@ + +index (thread-local-storage.index)

Package thread-local-storage

Package info

changes-files
license-files
readme-files