I just wanted to get these ideas down. I don’t expect anyone to implement them – I’m just putting them out there for consideration.
The model is of a bunch of coroutines running in the same thread and sharing a common structure (shared state). This is the actor pattern or similar. The coroutines are used by the main stack code to handle actions that need to span several ‘waits’, whether that be waiting for timers or external events or callbacks or whatever. The main stack code wakes up coroutines as necessary as events arrive (this can be in a library).
By coroutines I mean genuine cooperative coroutines with their own stacks, with yields under program control (i.e. not pre-emptive).
The key to this is the data shared between all the coroutines. Since the coroutines run in the same thread and the yields are well-defined, for the execution time between two yields, the code has exclusive access to the structure shared between the coroutines (the actor state, if you like).
If the compiler kept track of the yield-points, and internally marked any function that might yield as a potential yield-point all the way back through the call stack, then the compiler could allow borrows of a mutable pointer to the shared data for free (i.e. compile-time checked). Effectively the code has a lock on the data between yield-points, for free.
This could be done with runtime checks, but this is such a lightweight model it seems it could be worth optimising for. Single-threaded cooperative coroutines are very lightweight to start with (just a few instructions to switch stacks at a yield), and with effectively free locking provided by the static analysis, it becomes a very efficient approach to writing code which has to block, but which is neater coded in a serial fashion (with loops, nested function calls, etc, etc).
The alternative to this is to keep the same information that would be kept on the coroutine stack (loop counters, etc) instead in a structure, and feed events to handlers associated with that structure. But this quickly loses all sense of the flow and structure of the code. Each event handler fractionally advances the state of the structure, but the overall picture is lost.
This is not about parallel processing or even much about concurrency. Rather it is about rearranging event-handling code structure to make it as clear as possible. Coroutine stacks are used to hold state, and natural serial program structure is used, with the yields hidden in library functions, mostly.
(Context for these ideas is current paid work I’m doing in Lua, where Lua’s single-threaded coroutines have permitted a very clear expression of the code (after having tried other ways of expressing it). However in Lua I have to mentally keep a track of where yields happen, and where in the code it is safe to make state changes without risking races. In Rust it could be done a lot better.)
Requirements to implement this model (at maximum efficiency):
- Library support for creating/cleaning up coroutine stacks, switching to another stack (yield)
- Compiler support for temporarily borrowing shared state between yield-points with no runtime checks, which requires tracking yield-points