[ANN] Affect 0.0.0 – A streamlined and natural concurrency model for OCaml

Hello,

It’s my pleasure to announce the first release of affect:

Affect is a streamlined and natural concurrency model for OCaml.

It provides parallel asynchronous functions and first-class synchronous actions to orchestrate them. The resulting concurrency model has structured cooperative concurrency, structured cancellation and structured effect handling.

Affect is distributed under the ISC license. It has no dependencies. It optionally depends on the cmdliner library and the OCaml unix library.

The goal of affect is to provide a short and ergonomic concurrency model and composable synchronization primitives to program parallel and concurrent systems in OCaml. It is:

  • Streamlined, because it strives to minimize the exposed abstractions, terminology and conceptual overhead.

  • Natural, because it bases its central concurrent computational abstraction on the substrate of the OCaml language: functions.

If the now almost 40 years old CML events mean some thing to you, one way of seing affect is just: structured and cancellable parallel asynchronous function calls whose results can be synchronized with CML events (actions in affect).

If CML events don’t mean anything to you but Go channels do, then the take away is that those can be implemented with affect’s first-class synchronous actions. However actions give you more than that. They give you a general composable synchronization mecanism which you can understand as:

  • An extensible select.

In affect you can for example select to wait between the result of an asynchronous function, the cancellation of another one, a timeout, a signal delivery, a set-once cell becoming set, a file descriptor becoming readable and a funny primitive action of your own.

The lock-free implemention of actions follows this 2009 paper with some ideas taken from the Guile implementation of CML. Like the latter, one thing that affect does is to change the CML terminology. We find the event-based terminology to be very confusing for thinking about these mecanisms. To fully Milnerize OCaml, we drew inspiration from CCS and replaced event by action, and channel by port. See the design notes for more information about these naming changes.

Beyond trying to expose a clear and accessible concurrency model, what distinguishes affect from the first generation of OCaml 5 concurrency libraries is that:

  1. It fully embraces the new parallel nature of the OCaml runtime system and makes no distinction between concurrency and parallelism.

  2. It exposes its high-level abstractions, namely first-class synchronous actions and asynchronous functions, for others to be handled. And these mechanisms being parallel-safe and lock-free, not much work is needed to integrate them, little constraints are imposed on the context where they can be used.

This notably means that:

  • If you are unhappy about just using an invocation of the built-in scheduler in the main of your program or unhappy about using it at all, you can reasonably easily reclaim control over your application’s concurrent and parallel architecture while still using eco-system libraries that would make use these abstractions.

    For example this test has code and examples showing how to handle actions or running a root asynchronous function in a thread by yourself. Those can even interact with a parallel invocation of the built-in scheduler if that’s your thing. This other test shows that two instances of the scheduler can run along side, either in their isolate realm or even communicating via ports.

  • Since at some point it would be good to agree on more than bytes, it must be possible for existing schedulers to handle affect’s high-level abstractions or a subset of them. For example to handle actions, only a parallel-safe closure to schedule the work to unblock in your scheduler has to be provided at action invocation handling time.

TL;DR. You get a nice and ergonomic concurrency model and flexiblity.

Regarding the release, while I think that affect is now finally conceptually well rooted, it’s early time and it has not been used much. The aim of this first release was to have a fully working and usable implementation of a design that has been rotting for two years in a repo and in my thoughts. This means that in the current state:

  • You likely don’t want to use it in production[1].

  • Changes should still be expected. Especially if you fiddle with the private APIs. Early adopters and questions on this forum are welcome. Get in touch on the issue tracker if you are trying things and running into trouble.

  • The library will require the latest version of OCaml for a couple of releases.

  • One thing that didn’t make it to the release is to replace the use of select(2) in the cooperative Unix compatibility module. The initial plan was to use epoll/kqueue there, it’s still the plan.

  • The current implementation went for clarity and correctness, if not naïvety. If you don’t see shining performance, there are quite a few dimensions where it can be improved. Some structures can be specialized, more imperative data structures can be used (not always a win though), some atomics can perhaps be retracted, simplistic or dumb scheduling strategies can be made more complex, etc. And of course you can always BYOS (the mono-threaded implementation of the full model mentioned earlier is ~100 loc).

    The code base should be reasonably approachable, for now it’s 690 loc if you consider only the core modules for action and asynchronous functions; the parallel work stealing scheduler adds 380 more. But the devil is in the interleaving :–)

Happarapyllecomputling!

This first release was made possible thanks to a grant from the OCaml Software Foundation. I also thank my donors for their support. Everyone’s support being essential for these bits to get worked on and be distributed.

Homepage: https://erratique.ch/software/affect
Docs: https://erratique.ch/software/affect/doc or odig doc affect
Install: opam install affect (opam PR)

Best,

Daniel


  1. I’ll be comfortable telling you to do so when I have an affect-based connector for my IO agnostic HTTP library deployed on a live system :slight_smile: ↩︎

23 Likes

A couple more points:

  1. Before someone asks if this shouldn’t be implemented over picos, I will defer to that discussion. The TL;DR is no and there is no plan to do so.

  2. In contrast to other concurrency libraries, using affect’s asynchronous functions does not rob you from using effects in your code. See the cookbook for more information.

  3. If you’d like to see the cancellation model in action this robust combined ping client/server program and its chaotic canceller shows how the cancellation model fares in pratice. Especially the lack of bureaucracy around it.

Finally I’d like to quickly describe how the cancellation model of asynchronous functions is implemented because I’m really happy in how the pieces ended up falling into place. In my head I expected it would involve making use of CML’s negative acknowledgements which allow an action to be notified if it’s not the chosen one in a choice[1] – except that makes no sense :–)

Instead when an asynchronous function invokes an action with Action.invoke, the scheduler adds a hidden primitive action as a choice to the action being performed. This added action synchronizes on the cancellation of the asynchronous function by raising Fun.Async.Cancelled. It is a private version (for efficiency reasons) of the Fun.Async.wait_cancellation action that you can use to synchronize on the cancellation of an asynchronous function.

So one way of thinking about the interaction between the cancellation of asynchronous functions and action invocations is that when you (or a direct style function) invokes an action with:

Action.invoke action

what you end up effectively invoking, unless you are in a masked cancellation scope, is:

Action.invoke (cancellable action)

with cancellable conceptually implemented for the running asynchronous function call f as:

let cancellable action = 
  let raise_cancelled () = raise Fun.Async.Cancelled in
  let if_cancelled = Fun.Async.wait_cancellation f () in 
  let raise_if_cancelled = Action.map raise_cancelled if_cancel in
  Action.choose [raise_if_cancelled; action]

The nice thing about is that this does not affect (yes) the implementation of actions at all which does its business unaware of the asynchronous function cancellation model[2]. Nor does it affect the scheduler (except for passing the hidden action). So all that is nicely contained to the model of asynchronous functions.

This scheme is what brings us our world of pervasive EINTR – except this time in a hopefully useful and reliable way :–) for structuring and cancelling your functional activites.

In passing note that this makes affect’s cancellation model pass the “cancellation support test” of the F# people mentioned in section §2.4 of this paper (PDF).


  1. This is not implemented in affect for now. It could be added later, it’s one additional combinator and entails only internal changes, but it’s not super clean in my opinion, it exists in OCaml’s upstream Event implementation but with a different signature from CML. ↩︎

  2. We did however add an optional argument to the private function that creates action invocations to distinguish this action so that it doesn’t show up in the tracing metadata ↩︎

8 Likes

Looks like a cool project, thank you for working on and releasing this!

I love the little joke here about concurrency bugs! :sweat_smile:

2 Likes