They acknowledge that shared mutable state is (very) problematic in conjunction with preemptive multitasking, so they're actually arguing for cooperatively-multitasked threads as opposed to events. In both systems, shared-mutable state is being used between the concurrency code units.
I think they're neglecting a third option: Shared-immutable state, with very little, explicit, shared-mutable state. This is the model used by Haskell threads, and Erlang processes. Like Events/Cooperative-threads, it does away with the horrible semantics of shared-by-default, and gives the parallelism benefits lost when switching to cooperative multitasking.
But I think there are still 2 crucial differences between event-based systems and cooperative threads that I did not see in the paper:
* Events make it clear where context-switch/reentrancy points are. When I call a library function, then I generally have a very good idea if it could "call me back" (have I given it callbacks to call me back?) and if not -- I know it is safe to call it and do not have to reason about my internal state in every given call. Even with cooperative threads, a "yield" point may be lurking in any library you're calling. This effectively means that any library call can potentially do anything, including switch to "higher-level" code that may re-enter your own code. This makes re-entrancy considerations far more complicated. It is solvable, though, if you make sure that yield-points are "tagged" with some token that must be passed as an argument. Then, a function signature and call pattern tells you whether or not it can yield to anywhere.
* Implementing user-level threads may be very cheap, compared with standard posix threads. But it is still many times more expensive than using explicit data allocations as in event-based programs. Even if using "split stacks", a user-level thread is still going to pay with around a 4Kbyte stack. 1 million threads thus take 4GB of memory before they even do anything. This means that you still have to be wary of creating threads as freely as you would register callbacks -- and requires mixing in event-based programming, or manual event loops, into your threads. This mixture is worse than uniformity of any method.
The cooperative multitasking model of F#'s async makes good progress in solving your first point. There are essentially two ways of assigning the result of code blocks to variables: doing "let a = foo bar" which runs the code without a yield and "let! a = foo bar" which can yield as required inside foo. So you always know which library functions are safe (and you can even write unsafe ones yourself) and can write cooperative threads very easily (just as easily as usual threads).
Is that a language feature specifically tailored for cooperative multitasking?
That sounds good, except passing an argument ("yielder") that cannot be captured/stored (so you have to take it as an argument) sounds like it would require less language magic to solve the same problem.
You make some good points, but why would every lightweight thread require about 4k of memory? I know for a fact that the Haskell runtime doesn't need that much; it allocates a starting stack (default size 1k, but can be lower) and grows it if necessary. I imagine the techniques here would also apply to other languages.
4k is the paging granularity of x86, so if you want to be able to use hardware-assisted guard pages to know when to grow the stack, you have to allocate in multiples of 4k.
Haskell allocates 1KB (which is also much larger than the allocation required for a typical callback registration in event-based programming) initially, but grows the stack in chunks of 4KB.
These small chunks come at a cost, too. The GHC runtime needs to copy around the edges of the stack at the chunk boundaries, and stack use becomes slightly more expensive.
I think they're neglecting a third option: Shared-immutable state, with very little, explicit, shared-mutable state. This is the model used by Haskell threads, and Erlang processes. Like Events/Cooperative-threads, it does away with the horrible semantics of shared-by-default, and gives the parallelism benefits lost when switching to cooperative multitasking.
But I think there are still 2 crucial differences between event-based systems and cooperative threads that I did not see in the paper:
* Events make it clear where context-switch/reentrancy points are. When I call a library function, then I generally have a very good idea if it could "call me back" (have I given it callbacks to call me back?) and if not -- I know it is safe to call it and do not have to reason about my internal state in every given call. Even with cooperative threads, a "yield" point may be lurking in any library you're calling. This effectively means that any library call can potentially do anything, including switch to "higher-level" code that may re-enter your own code. This makes re-entrancy considerations far more complicated. It is solvable, though, if you make sure that yield-points are "tagged" with some token that must be passed as an argument. Then, a function signature and call pattern tells you whether or not it can yield to anywhere.
* Implementing user-level threads may be very cheap, compared with standard posix threads. But it is still many times more expensive than using explicit data allocations as in event-based programs. Even if using "split stacks", a user-level thread is still going to pay with around a 4Kbyte stack. 1 million threads thus take 4GB of memory before they even do anything. This means that you still have to be wary of creating threads as freely as you would register callbacks -- and requires mixing in event-based programming, or manual event loops, into your threads. This mixture is worse than uniformity of any method.