On concurrency models

Indeed it’s the only way to get to a real compositional, multi-core friendly eco-system.

It’s quite clear that the current breed of multi-core abstractions and their various pools, is totally ill suited. And picos, despite all the excellent work by @polytypic that goes in it, by enabling them to live along each other is making things even worse: there’s nothing you can do when you have multiple, very different, concurrency models in your program (e.g. IIRC miou checks stuff about your ressources dynamically). Basically it’s not solving the right problem, the problem is not to enable concurrency model to co-exist, the problem is that there are multiple concurrency models :–)

Effect based, user defined concurrency, sounds like a good and cool thing, but I think they are a real disaster for the language and eco-system. The concurrency model should be mandated by the language and be a property of the runtime system. That in turn allows programmers to develop proper compositional mental models for it and dedicated tooling for dealing with the curse of concurrent programs.

With respect to this while I’m curious about Eio’s capability system (though I acknowledge the fact that it seems users are finding it painful) I’m definitively not convinced about its concurrency model (which is a separate thing).

I recently made a new design round on affect and tried to pin down a concurrency model that can fit on a page and hopefully in the user’s head. It tries to minimize the number of concepts (it seems programmers are extreemely keen on inventing new terminology and structures when they have to deal with concurrency) and fit into OCaml’s exisiting paradigms. Namely we introduce just one thing: parallel asynchronous function calls (in fact things like “structured concurrency” become self-evident when you think in term of function calls, a function always returns after all its functions have executed those being synchronous or asynchronous). See the design notes. There’s nothing ready to use yet but I think it’s the kind of fixed concurrency model I would like to eventually program with in OCaml and being mandated by the runtime system.

8 Likes

I don’t particularly have the motivation to write a long post again, but I’m a bit vexed by this specific part and the trivial dismissal of existing solutions. Dedicating entire domains to each library is indeed terrible, but I’ve been proposing to use Thread.t pools on top of a single domain pool for a year and a half now. We have the one good abstraction and it’s called threads. All we need is to entirely forget about the bad abstraction named Domain.t, treat it (like Go does) as an implementation detail, just like a CPU core, and provide libraries with:

val pool_size : unit -> int

val spawn_on : int -> (unit -> 'a) -> 'a Thread.t

and that’s it. That’s all you need. I have an implementation of that in moonpool, but it really should belong in the stdlib.

5 Likes

It’s not a trivial dismissal, I’m just not convinced by what you propose. I don’t think that making explicit scheduling choices on pools (Fut.spawn ~on) is a good idea and compositional.

I’m not even proposing to make moonpool the standard (it’s likely too big for that), so Fut.spawn ~on is out.

I’m more specifically talking about using the domain pool underlying all thread pools to start threads. Threads are compositional, domains are not, that’s all there is to it.

1 Like

It’s roughly the direction taken by Java and the new virtual threads abstraction. By reusing threads, they allow the programmers to keep a abstraction they are already familiar with, so no need for a new terminology :slight_smile:

One advantage of a library like Picos is that it enables interoperation for users of a concurrency library, e.g. you can have multiple data-structures or libraries that want to expose a concurrency-friendly abstraction.
With monads you “only” had about 2 major concurrency libraries to support (and that already was a maintenance burden), but there are already 7 or more effect based concurrency libraries. Unless your entire application can be implemented by just using what the concurrency library/framework provides, you’ll likely want to use multiple libraries written by different people. And if each of them picked a different concurrency library then you’d be a bit stuck if you want to write an application, you’d have to port at least one of them to another framework.

I agree that you should probably only have exactly one effect scheduler (or “engine” as Lwt would call it) in an application (except for transition or testing purposes).

No disagreements there, the interoperability of multiple effect based concurrency models belongs in the standard library (and I think one of the longer term goals of Picos is to explore what that interface should be). But it is probably better to explore the design and interoperability for such a library externally first, and once it has proven to have been adopted/ported by multiple frameworks then adopt it into the standard library. That way we know it is not missing some fundamental abstraction that would require an incompatible rewrite.

I think it is useful to make a distinction between a concurrency programming model/semantics (where clearly no single design can satisfy everyone’s requirements or we wouldn’t have 7 alternatives already), and a concurrency runtime (or interop interface).

E.g. you don’t need to make a choice in the runtime on what cancellation semantics you want, or whether it is supported at all, or how to deal with resources, etc.
What you need is a cooperative way to block, to spawn new fibers, and if you implement cancellation then a way to cooperatively handle that.

i.e. there needs to be an agreement on the interface between users of concurrency abstractions.
Looking at Picos this amounts to 5 effects GitHub - ocaml-multicore/picos: Interoperable effects based concurrency

I’ve been thinking whether this is indeed the minimal set, or whether it could be made even more minimal, but I don’t have good idea to propose a change here. Every time I think that one effect could be removed, I can come up with a counterexample of an application that is not implementable if that effect is removed.

For example, although Yield is very useful for testing, is it really required for an application? OTOH if you can’t deterministically test your applications then you won’t have a (well behaving) application. Can you express the semantics of Yield by an artificial trigger that self-triggers after one scheduler cycle?

Similarly does Fiber.Current really need to be there? It is useful for fiber-local storage and cancelation, so I don’t see a good way to remove it, although it feels like it should be more implicit.

3 Likes

Big disagreement here :–) There should be no multiple effect based concurrency models.

I don’t think that trying to make a distinction between the language, the concurrency model, and the runtime is productive.

The details of these primitive effects you are talking about shouldn’t even matter, it should be abstracted by the standard library and only perhaps exposed to those who want to write new schedulers (and which you can perfectly indicate you are going to break them if you are unclear at what is the best way to implement your concurrency model so far).

Programming concurrent/parallel programs is a mess. You want your language to mandate one way of describing these programs, that is one model. At least that’s what one would expect from a helpful programming language. You don’t want seven models, i.e. seven time the mess. You need to make a choice, you need your language to be designed, not to be defined by piling up features.

Once your programming language has defined a fixed concurrency model, you can actually optimize your runtime system for it and develop tools for it to help programmers deal with the mess. It will also allow them to shape mental models for it and work with it in a compositional manner.

When I wanted to clarify the semantics for cancellation in affect this autumn, my searches lead me look to look into Swift’s concurrency model and trying it a bit I was quite surprised by all the hints the compiler would give you about data race safety, there’s a reason for that: a single, mandated, concurrency model.

I don’t think that allowing seven different concurrency models and the standard library providing tools to enable them to co-exist makes OCaml attractive in any sense. On the contrary, it rather shows that it keeps on repeating the same errors.

6 Likes

I don’t even agree with that :slight_smile: . I think an IO oriented scheduler, and batch processing scheduler, have different design tradeoffs, and I want to freely mix them in an application.

It’s also useful for fairness, if you’re running a costly task on a shared scheduler it’s good to yield regularly to give other fibers a chance?

That’s how I think of picos. Picos takes the mantle from the dusty floor where the core OCaml team had left it, and defines a reasonable concurrency model using fibers, computations, and triggers. You can build more exotic stuff on top of it (structured concurrency for example, or fork-join) but it gives everyone a shared meaning of what it means to complete/cancel/await a computation.

For sure, fragmentation sucks. But it’s going to keep happening unless there is actually a language-defined concurrency paradigm (my forecast for this is circa 2047) or unless we agree to build a common foundation, ie picos. :person_shrugging:

4 Likes

And even beyond, given the fact that OCaml ships with a standard library and metaprogramming system, and there are third-party alternatives even to those :wink:

1 Like

I think we all agree with @dbuenzli that it would have been much better for OCaml 5 to have determined a single concurrency model. Given the fact that it didn’t, we need to try and do the best we can to whatever degree is possible.

I’d like to highlight some additional issues/thoughts:

  1. Figuring out which model is best via competition outside the runtime sounds nice in theory. In reality, there are very few clients for this stuff. Most OCamlers (and probably most of the core team) aren’t going to be touching much of this anytime soon. Among the applications within the domain, lwt still dominates. So instead of a good, solid evolutionary process, we’re getting a process of genetic drift, where the decisions of a few designers and a tiny crowd of users is causing effects that will have major consequences for the ecosystem later on down the line.

  2. @c-cube and I were discussing this earlier, and we have 2 main competitors right now: eio and miou (as well as moonpool, which chooses to do things very differently). eio has tacked on controversial design decisions such as capabilities - as if we actually needed more complexity right now - and also doesn’t really have a coherent multicore story. miou has a better multicore story but has its own controversial decisions, such as supporting only select. Does miou support work stealing? Is it really optimized for multicore? Not sure. The idiosyncrasies in these 2 systems are precisely the kind of thing that creates ‘genetic drift’ – odd decisions that determine our future and can hurt the community long term.

  3. AFAICT, the handling of the limited resource called domains ultimately means that no matter what you do in the concurrency space with effects, you want to have one resource manager for domains, which ultimately means one scheduler. You also want to have work stealing if you can have it, which again means tight integration between effect-based scheduling and domain management. It also means a scheduler has to have a solid multicore-management story.

  4. We don’t have to/want to reinvent the wheel. OCaml lacks systems/multicore people (both producers and consumers), and we’re late arrivals to this space. The most logical thing for us to do would be to copy the things that worked well in other ecosystems. The most prominent example I know of is tokio in Rust. Looking at the 2 major alternatives, we lack an alternative that is as good as tokio (I’m not very experienced with it myself so I’ll let @c-cube elaborate). Again, moonpool is neat, but I don’t think we want threads to remain our main concurrency primitive, even if it’s often convenient.

4 Likes

we have 2 main competitors right now: eio and miou

I’d like to emphasise this sentence. As far as Robur and Miou are concerned, we don’t see ourselves as a competitor to Eio. In fact, we don’t consider Eio to be bad but, as it says in Miou’s genesis, our aim has been to separate ourselves (by creating a new scheduler) from Eio because we don’t want to fit in socially with what Eio represents (in its proposal and in its method of adoption).

In this respect, and I’ve been able to say this publicly several times, we’ve had various differences of opinion with the Eio team that aren’t technical. Being a small team at Robur, we decided, precisely 2 and a half years ago, to save our brain time because we were not satisfied with the interactions we were able to have with the Eio team.

the problem is not to enable concurrency model to co-exist, the problem is that there are multiple concurrency models

I’d also like to reiterate what we’ve been saying for quite a few years now. The fragmentation of the community across several schedulers didn’t pose any particular problem for us at the time of Lwt/Async. It still doesn’t pose a problem for us at this stage, despite the saturation of schedulers for OCaml 5. We think of our libraries independently of schedulers and our approach has worked for over 10 years. I’ve already said that we’ll continue this approach even if we develop Miou ourselves. We could be more authoritarian and consider that all our interactions with the system in our libraries should go through Miou but our experience shows us day after day that the time spent making libraries independent from schedulers is a good approach for the community in general.

A home-made composition of these libraries with any scheduler is often quite satisfying (especially when it comes to grasping the specific advantages of these schedulers).

As far as I can remember, the history of compatibility between schedulers does not date back to OCaml 5 but attempts have been made in the past. My opinion (and I may be wrong but I tend to think so) is that it is indeed impossible to reconcile several models of cooperation even if partial efforts can be made in this direction at the community level. In this case, the trials between Picos & Miou have confirmed this observation more than the other way round. The past also confirms it.

I also think that Picos goes too far in what it proposes and already has a certain opinion of what a scheduler should be. Here too, like Eio, an attempt has been made to develop our libraries in a way that would satisfy the community. Unfortunately this attempt failed because of trivial issues. And here again, as a small team unable to manage several fronts, we decided not to make any more effort than was necessary.

More generally, I think that two trends are emerging on the question of schedulers. There is one approach, which I would call authoritarian, where the idea is to propose a single model of task cooperation that will satisfy as many people as possible. It seems accepted that the latter should be integrated into OCaml, a point on which I agree. One could imagine Picos as a candidate but it has been confirmed to me that the Picos developers do not wish to interact with the OCaml core-team. Eio also took this approach at the beginning, presenting itself as the scheduler for OCaml 5 and having a fairly aggressive communication compared to the other proposals.

Personally, and from my experience with Lwt and Async and my opinions too, I’m not too keen on this approach preferring diversity of solutions and the possibility of choice.

The other approach is to accept the situation as it is. Again, as far as Robur is concerned, this situation is not unsatisfactory. Eio supports co-exist with those of Miou and Lwt in our libraries. Switching from one scheduler to another is not a problem for us. It may be unsatisfactory for new users, but I think that from this particular point of view, even a new user can identify and understand the difficulty of homogenising an entire community on a single scheduler. They can also understand the frameworks within which the various schedulers have been developed.

As it happens, some articles and discussions I’ve had with others about the choice between Lwt and Async are essentially based on social and concrete realities (maintenance of the scheduler by a large company or a small cooperative, documentation, examples of use, interactions with maintainers, etc.). And if we had to specify the subtle and technical differences that there may be between schedulers in order to make a choice, a third possibility exists (just as legitimate): you could make your own scheduler! And this is perhaps the big difference between OCaml 4 and OCaml 5, with the effects, it’s easy to make a new scheduler.

its own controversial decisions, such as supporting only select.

Miou was designed (as was Affect, I think) for strict separation between the system polling method and the scheduler. The very basic idea behind such a separation (which also concerns Lwt by the way) is that we don’t have Unix.select on our unikernels.

Extending Miou with epoll or io_uring is possible. Unix.select was chosen because it is the easiest and quickest to develop.

The most prominent example I know of is tokio in Rust.

I don’t think the grass is any greener elsewhere. Rust also has a fragmented community when it comes to schedulers (radeon, smol, embassy, glommio).


This is where I’d like to re-clarify Miou. Miou is based on objectives linked to our cooperative: developing services and unikernels. That’s the case with all the other proposals for that matter - but I’d be careful not to say what their objectives are. Miou’s ambition is not to satisfy as many people as possible or to compete with anyone else. Its aim is to satisfy, in this case, 4 people first and foremost, the members of our Robur cooperative - which is not bad at all! And that’s all… Then we can try to communicate about Miou and show that it’s a solution that at least works for us. But we won’t pretend that it’s the solution.

Finally, if people want to participate in Miou, they can, as is the case with all our other projects. Our interactions with users over the years are proof of our openness to contribution and compromise. But if people want to use something else, that’s their fundamental choice.

13 Likes

Intent doesn’t really affect the experience of the consumer of the library. Miou is an alternative product that competes with Eio and Moonpool.

Does this generalize to multicore? Can an high quality multicore management scheme be attained - particularly one with resource constraints such as the ones we have in domains - when different schedulers are cooperating?

That’s awesome to hear.

A quick search suggests that tokio has near total dominance at this point. To me, this suggests a pretty good library that we should strive to copy from if we can. A tokio-like alternative would at least give us a comparison point to the other existing libraries.

As an aside, I believe this level of communication, criticism and honesty is critical for OCaml’s success. OCaml has many alternatives out there, and in order to make sure OCaml succeeds as much as possible, we have to maximize our communication and offer as much constructive criticism as possible. At the same time, I appreciate the huge amounts of effort that have been put into every library.

9 Likes

Just to elaborate on the situation in rust, tokio is the most popular scheduler indeed. Embassy is popular in embedded, and there’s other niche schedulers. All schedulers use the standard Future trait (which would be the equivalent of a good chunk of picos, and provides compatibility on the .await front).

What’s missing in rust is a compatible async IO abstraction (async read, async write, etc) and afaik it was waiting on async traits/HKTs. It’s now possible to represent these things so it’s possible that in the next few years there’ll be a scheduler agnostic way to read and write.

3 Likes

I think the issues for newcomers arise when there is no dominant solution.
If we take the build systems as an example, dune is the de facto default build system for OCaml. It’s far from the only, and people are free to create new ones. But most code out there use dune, most newcomer documentation/articles use dune, the editor plugins treat dune integration as a priority. A newcomer can focus on learning OCaml, even if they have to copy and paste some mysterious sexp expressions.
Contrast that with Lwt/Async, one of our major book uses Async, but most blog post out there use Lwt. Some libraries are compatible with one, but not the other. So a newcomer is forced to pick a concurrency library when they barely know how to write hello world…

The problem with multicore, the only library that has officially tried to be this “strong default, but alternative exists” is Eio. But its design hasn’t been well received by the community, and the Eio devs have been unwilling to work with the community to address some of the concerns. (that’s based on what I can read here, I haven’t directly interacted with Eio dev process)

I’d rather change the blessed multicore approach once a decade than try to pretend we can have 5 major multicore schedulers with top-notch support across libraries, tools and documentation.

5 Likes

I don’t think that’s the case. Miou is developed by the Robur team and imho their intent and opinion are the primary thing that matters for its future direction and development. Eg, if some people want to add a new feature to Miou, but it doesn’t make sense for Robur, then likely that feature won’t get added.

I think we should respect what the folks developing these systems are saying:

  • Miou is primarily for Robur but it may work for others
  • Picos is a building block for a concurrency library but is not ready to become a standard yet
  • Affect is in a design phase right now but is intended to become a community standard
  • Moonpool is also intended to become a standard and promote using the existing abstraction of lightweight threads running on top of domains
  • Eio is also intended to be the community standard and is the most mature of all of the options, but of course its issues have been widely discussed already.
9 Likes

From the point of view of a user of schedulers, not implementor, absolutely nothing. I think a world with 5 schedulers based on Picos is a bit better than one with 5 schedulers with their own abstraction for a fiber.
I’d go even further, and agree with @dinosaure, a world with 5 schedulers, even not compatible, isn’t that bad.

My position is more social than technical.
As @dbuenzli aid, having a clear model for concurrency is important to implement applications that are correct and efficient. We even touched on some of these points in this thread, e.g. what’s the cost of creating a fiber, or does my scheduler uses work stealing or not. This a not just implementation details, they will matter for the performance of the code we write and shape our solutions.
So ideally that model should be shared not only in the concurrency library documentation, but in blogs and tutorials to be easily accessible for all. We should have a large enough pool of experts here and discord/IRC to answer questions.
For a community the size of the of the ocaml community to do it well once is a difficult task. To do it well 5 or 7 times is impossible.
In that world with a largely agreed upon concurrency model, if unikernels need to be different, the same way rust embedded needs to be different, then so be it. Niche things sometimes requires different approaches and it’s ok.

2 Likes

Thanks @polytypic for your comments, very much appreciated!

Some are likely spot on. For example I’m not super attached and convinced on the current blocking mecanism (as is written in my todo) – in fact I would like eventually to get to a compositional synchronisation mecanism like CML provides. But I’m not sure your objections about the structure I want to enforce are as problematic as you make it sound (or that I’m not willing to pay a reasonable cost for it in exchange of a good mental model), but we likely have different agendas here and, more importantly, the proof is in the pudding, and there is absolutely no pudding yet[1] :–)

For the rest of the larger discussion, I feel myself a bit off. I’m mostly interested in ergonomics and design of usable systems and good mental models for programming, none of which I find either in this discussion or the systems that are being talked about.

But I just want to emphasis that:

The day your application needs to pull two libraries each relying on another concurrency model, then you don’t really get a choice. And that is the problem.


  1. I usually do not pre-announce work. Affect got out just to provide a design counter point in the long discussion that followed eio’s first release. ↩︎

6 Likes

Aaah but this is where things finally get interesting! Now we talk about design which is about saying no and making choices – in my view there is no “opinionated” system, there are those systems that are designed and those that are not :–)

So I’m interested on the why of:

(In affect I did not rule out allowing to detach parallel asynchronous function calls from their parents. I can see it as being useful, albeit it think should be the exception not rule. In affect I just want to introduce one concept that I feel is natural for OCaml: parallel asynchronous function calls; the strict parent-child relationship (a.k.a “structured concurrency” nowadays) fits then naturally with synchronous function calls)

1 Like

For those interested, we’ve spent some time writing a book on how to use Miou and asynchronous programming with Miou — basically, it introduces Miou’s design. In addition, resources that may be of interest are:

  • a retrospective of a handheld scheduler implementation compared to what Miou offers
  • a manifesto that says the same thing as what I said above

A next release of Miou is in preparation and additions to this ‘little book’ will be made. In particular, there will be an explanation of how we implemented happy-eyeballs, which remains a good example.

5 Likes

Sorry but I have not been following this discussion too deeply, but I wanted to bring up something here about our our library, Abb, works, which is derived in a good chunk from your earlier work with Fut. We have a parent-child relationship when executing work. However, as pointed out, sometimes it is necessary to adjust that relationship, so we have a fork function:

val fork : 'a t -> 'a t t

That has worked pretty well (but really battle tested with anyone other than me).

1 Like