HACKER Q&A
📣 nrdvana

What languages have true native cooperative multitasking?


and also, "how well did it work?"

But let me clarify exactly what I'm asking here, because concepts like "coroutines" or "cooperative multitasking" or "multiprocessing" get heavily influenced by their implementation in each language, and I'm asking for a very specific implementation.

In a stack-based function-call language, there is usually a simple API of blocking I/O where the state of each in-progress function is preserved on a stack as the entire thread of execution is paused until the completion of the I/O. This leads to very clear and easy-to-debug programs, usually with the added convenience of throwing and catching exceptions, but you can only process one I/O sequence (client/server conversation, etc) per thread. Moving to multiple threads or parallel processes lead to complex concurrency problems, and often don't scale well.

Languages usually also provide more complex secondary APIs for non-blocking I/O that allows a function to start an I/O operation and continue doing other things while checking the status of the I/O, or blocking on an event API to wake up when something is ready. Taken to its logical conclusion, the program becomes structured around an event loop, but the programmer must now track the on-going state of a particular I/O sequence in their own data structures without the convenience of a stack-based tree of function calls. You can no longer use normal language exceptions to abort a single I/O conversation because it is interleaved with other conversations and the logical call-tree is gone. The program must also avoid using any library which makes use of blocking APIs, dividing the library ecosystem into "blocking" and "nonblocking-compatible" halves.

One solution is to add co-routines to a language, where multiple function-call tree stacks are held in memory, and executed on a single thread. Any time a blocking API is used, the language starts the I/O operation then switches to another co-routine that is ready to run. In other words, the language manages the event loop hidden in the background while presenting the user with what appears to be normal threads, but without most of the hard synchronization that you need from true multithreading. The downsides are that an infinite loop on one "thread" will hang all others, and there might still be synchronization issues if I/O operations are interleaved with changes to data structures.

Most scripting languages provide something for co-routines, but usually requiring some invasive changes to the code that are not just the simple blocking APIs presented by the stock language. This leads to further fragmentation of the library ecosystem for the things that are "async compatible" or "coro compatible" etc.

Python: the generators act like independent call stacks, and were made into "co-routines" with a send() call, but these do not tie into the native blocking API read/write/sleep to perform event-driven I/O like I described above. The co-routines are also used like objects rather than called like normal functions.

JavaScript: JS has the Async/Await system which is almost what I describe, but they are exposed as keywords which must be used in the code, not passively compatible with other code using blocking I/O.

Perl: there is a Coro library that switches the interpreter stacks, but does not intercept the blocking I/O. There are several "AsyncAwait" modules, with the same caveat as JS.

There are so many languages out there, I'm sure that someone has already implemented intrinsic co-routines + non-blocking I/O exposed to the user as a simple blocking cooperative threads. I want to know which languages have tried this, and maybe more importantly, if you have worked on a project that used them, how well the idea works in practice.


  👤 matt_s Accepted Answer ✓
Elixir has concurrency built in as an easy to use concept. There is the concept of processes running within the runtime, which is called the BEAM. Think of the runtime as a mini OS where you can create lightweight processes as needed to do things asynchronously. You can use a simple Task module[0] to do things async or implement GenServer[1] which is a process running and has state.

An example is calling an API that has a paginated response where it indicates the number of pages (like ?page=X in GET request), you can async call each page and accumulate the responses using a map_reduce function[2]. You could fire up like 1000 (or 10k or as many as you like really) of these processes calling an API with different params/creds and handling paginated responses concurrently, each with their own state. I found that Elixir will let you quickly exhaust any rate limit budget/threshold you have in a scenario like this, that is more the limiting factor.

[0] https://hexdocs.pm/elixir/1.13.4/Task.html

[1] https://hexdocs.pm/elixir/1.13.4/GenServer.html

[2] https://hexdocs.pm/elixir/1.13.4/Enum.html#map_reduce/3


👤 retrac
Coroutines fit into some functional languages fairly naturally. They can be constructed out of the primitive operator call/cc (call with current continuation) in Scheme and simple cooperative threading, exceptions, callbacks, coroutines, etc. are usually implemented that way in Scheme, and pretty widely used. It meshes okay I guess with the rest of the language and libraries, but Scheme doesn't really have much in the way of libraries.

Coroutines are a type of monad in Haskell. If you want function calls to alternate between execution streams, that sort of chaining of execution, is what Haskell's monads deal with. Instance the type you're using in the Coroutine class, and any functions with the same Coroutine type, will automatically work as coroutines between each other when you suspend them. It's part of the standard library in Control.Monad.Coroutine. Since it's a monad, it composes with other monads, and will work with most library code. As to IO, with a lazy evaluation language, when a blocking call is made, other parts that can still evaluate (not just pending coroutines but also regular functions not depending on the blocking call) can continue, so all IO in Haskell sort of works as you describe there, I think. Lazy evaluation is usually implemented with coroutines, IIRC. In a sense, every value in the program is in its own coroutine, that runs when the value needs to be calculated because some other value depends on it. Analyzing the efficiency of lazy programs suffers the same kind of problem you mention with concurrent programs, to be fair.