Effects
@art-w suggests adding a Yield effect, and indeed itās a natural idea: the stdlib could define basic concurrency effects like Yield, define default handlers for them (āat the domain levelā), and let user-level concurrency libraries override them with their preferred semantics. (Those libraries even have the choice of sending some effects up to the default handler, etc.)
Yield
But what would be the API? The first idea is just
effect Yield : unit eff
This effect suspends the current computation, which can be resumed by just passing a unit value. But I think that this is not good enough, because this doesnāt give us any way to tell the scheduler when to restart the computation.
The protocol would be that if you resume a yielding fiber and itās not ready yet (in the case of concurrent lazy: the other domain is not done computing the value yet), it yields again.
I donāt know much about implementing efficient schedulers for cooperative concurrency, but my intuition is that this approach does not scale. If you have many fibers, many of which yielded, you are going to waste a lot of time checking which can run again. We rather want a design where we are ātoldā when a fiber is ready again (push-based rather than pull-based, if you want). (I guess one could do clever things with exponentially-decreasing priorities, etc., but it sounds like we are working hard to solve a problem that we created ourselves in the first place with a bad API.)
So we rather want to have an effect like
Yield : notifier -> unit eff
where the yielding computation provides a ānotifierā (a name I just made up) that the scheduler can subscribe to, to be notified when the fiber can be resumed. (It should be efficient to subscribe to a large set of watchers.) So the scheduler would typically have a poll/select-like loop to wait on the next blocked fiber to become ready.
Promises
Another way to think about the problem is that we should not design for āyieldingā, we should design for āblockingā. In the concurrent-lazy example: instead of thinking of an API when we notice that another domain is already forcing the value, we should thinking of the API for the first domain to say: "Iām entering a blocking section that other domains/fibers/whatever will want to wait on.
Basically that sounds like a āpromiseā API. We could implement a concurrent lazy thunk as follows:
type 'a lazy = 'a cell Atomic.t
type 'a cell =
| Thunk of (unit -> 'a)
| Forcing of ('a, exn) result Promise.t
| Done of ('a, exn) result
let force laz =
match Atomic.get laz with
| Thunk f -> ...
| Forcing prom -> Promise.await prom
| Done res -> res
Of course, we are just shifting to a slightly different problem: instead of designing agnostic blocking, now we need to design agnostic promises. A āpromiseā is a blocking computation that also returns a value (instead of just ābecoming readyā), but maybe this point of view can help with the API design.