It just renamed itself to asynchronous programing. That's quite literally what an 'await' is.
It just renamed itself to asynchronous programing. That's quite literally what an 'await' is.
Your opinion vs. my opinion, obviously. But the user reports of the experience in Rust is hardly even close to unanimous praise and I still say it's a mistake to sit down with an empty Rust program and immediately reach for "async" without considering whether you actually need it. Even in the network world, juggling hundreds of thousands of simultaneous tasks is the exception rather than the rule.
Moreover, cooperative multitasking was given up at the OS level for good and sufficient reasons that I see no evidence that the current thrust in that direction has solved. As you scale up, the odds of something jamming your cooperative loop monotonically increase. At best we've increased the scaling factors, and even that just may be an effect of faster computers rather than better solutions.
i suspect that this will end up being the paradigm that wins out, even though it isn't popular today
Since I've given up on monetizing this project, I may as well just link to its current state (which is very rough, the STM API described in the website is only partly implemented, and there's lots of cruft from its previous life that I haven't ripped out yet). Note that this is a fork of the previous (now MIT-licensed) Gaia programming platform (https://gaia-platform.github.io/gaia-platform-docs.io/index....).
https://github.com/senderista/nextdb/tree/main/production/db...
The version of this code previously released under the Gaia programming platform is here: https://github.com/gaia-platform/GaiaPlatform/blob/main/prod.... (Note that this predates my removal of IPC from the transaction critical path, so it's about 100x slower.) A design doc from the very beginning of my work on the project that explains the client-server protocol is here (but completely outdated; IPC is no longer used for anything but session open and failure detection): https://github.com/gaia-platform/GaiaPlatform/blob/main/prod....
I read that as octal; so 1024 in decimal. Not a very interesting year, according to Wikipedia.
So sometime between "02000" and "02999"?
A core problem is that it's now clear most apps have hundreds or thousands of little tasks going, increasingly bound by network, IO, and similar. Async gives nice semantics for implementing cooperative multitasking, without introducing nearly as many thread coherency issues as preemptive.
I can do things atomically. Yay! Code literally cooperates better. I don't have the messy semantics of a Windows 3.1 event loop. I suspect it will take over more and more into all walks of code.
Other models are better for either:
- Highly parallel compute-bound code (where SIMD/MIMD/CUDA-style models are king)
- Highly independent code, such as separate apps, where there are no issues around cooperation. Here, putting each task on a core, and then preemptive, obviously wins.
What's interesting is all three are widely used on my system. My tongue-in-cheek comment about cooperative multitasking winning was only a little bit wrong. It didn't quite win in the sense of taking over other models, but it's in widespread use now. If code needs to cooperate, async sure beats semaphores, mutexes, and all that jazz.
[1]: Asynchronous programming is not the only form of cooperative programming. Usually cooperative multi-tasking systems have a special system call yield() which gives up the processor in addition to io induced context-switches.
Assuming your type isn't an Awaitable, with magic methods to influence how the compiler actually generates the state machine.
i think it's safe to say that the number of personal computers running operating systems without preemptive multitasking is now vanishingly small
as i remember it, oberon didn't support either async/await or cooperative multitasking. rather, the operating system used an event loop, like a web page before the introduction of web workers. you couldn't suspend a task; you could only schedule more work for later
The key thing about 2023-era asynchronous versus 1995-era cooperative multitasking is code readability and conciseness.
Under the hood, I'm expressing the same thing, but Windows 3.1 code was not fun to write. Python / JavaScript, once you wrap your head around it, is. The new semantics are very readable, and rapidly improving too. The old ones were impossible to make readable.
You could argue that it's just syntactic sugar, but it's bloody important syntactic sugar.
Yes, of course you could, since everything beyond, uh, paper tape, next-state table, and current pen-position (or whatever other pieces there are in a theoretical Turing machine) is basically syntactic sugar. Or, IOW, all programming languages higher than assembly are nothing but syntactic sugar. I like syntactic sugar.
(But OTOH, I'm a diabetic. Gotta watch out for that sugar.)
i hear they're concerned about cancer of the semicolon
That's my point, we still do that. And based on your phrasing we're forgetting it :)
possibly you are using the terms in subtly different ways so it appears that we disagree when we do not
If the OS forcefully switches control it's preemptive.
in the mac os 8 documentation, explaining how mac os 8 only has 'cooperative multitasking', the term is defined in the way i'm using it (https://developer.apple.com/library/archive/documentation/Ca...):
> In programming, a task is simply an independent execution path. On a computer, the system software can handle multiple tasks, which may be applications or even smaller units of execution. For example, the system may execute multiple applications, and each application may have independently executing tasks within it. Each such task has its own stack and register set.
> Multitasking may be either cooperative or preemptive. Cooperative multitasking requires that each task voluntarily give up control so that other tasks can execute. (...)
> The Mac OS 8 operating system implements cooperative multitasking between applications. The Process Manager can keep track of the actions of several applications. However, each application must voluntarily yield its processor time in order for another application to gain it. An application does so by calling WaitNextEvent, which cedes control of the processor until an event occurs that requires the application’s attention.
that is, this requirement that each task have its own stack is not just something i made up; it's been part of common usage for decades, at least in some communities. the particular relevant distinction here is that, because each task has its own stack (or equivalent in something like scheme), multitasking doesn't require restructuring your code, because calling a normal function can yield the cpu. in the specific case of macos this was necessary so that switcher/multifinder/process-manager could multitask mac apps written for previous versions of macos that didn't have multitasking
what term would you propose for what i'm calling 'cooperative multitasking', like forth and mac os 8 and windows 3.1 (https://softwareengineering.stackexchange.com/questions/3507... https://retrocomputing.stackexchange.com/questions/791/how-d...)? this terminology is not absolutely standardized, and i'd be happy to use different terminology in order to be able to communicate productively
also could you please answer my request for clarification in https://news.ycombinator.com/item?id=38861074
Those are implementation details. What's actually happening in all cases is my definition.
> also could you please answer my request for clarification in
Yes, your examples or the 5 million other implementations of event loops. You forgot to add gtk's for example :)
> in the mac os 8 documentation
... and I see no mention of stacks on Wikipedia:
https://en.wikipedia.org/wiki/Cooperative_multitasking
Which lumps in both the explicit event loop style and the syntactic sugar that got added later.
We could throw definitions around till kingdom come like this. And it's not the exact definition that's my problem.
i'll see if i can flesh out the wikipedia article a bit
And why do you think that in the case of an explicit event loop you don't have to yield? You do have to, and have to sort out some way to continue on your own. Which makes the new 'syntactic sugar' approaches much easier of course. Doesn't mean the principle isn't the same and they don't deserve the same name.
for (;;) {
int r = GetMessage(&msg, NULL, 0, 0);
if (!r) break;
if (r == -1) croak();
TranslateMessage(&msg);
DispatchMessage(&msg);
}
or, in yeso, for (;;) {
yw_wait(w, 0);
for (yw_event *ev; (ev = yw_get_event(w));) handle_event(ev);
redraw(w);
}
async/await doesn't always hide the event loop in that sense; python asyncio, for example, has a lot of ways to invoke the event loop or parts of it explicitly, which is often necessary for integration with software not written with asyncio in mind. i used to maintain an asyncio cubesat csp protocol stack where we had to do thisto some extent, though, this vitiates the concurrency guarantees you can otherwise get out of async/await. software maintainability comes from knowing that certain things are impossible, and pure async/await can make concurrency guarantees which disappear when a non-async function can invoke the event loop in this way. so i would argue that it goes further than just hiding the event loop. it's like saying that garbage collection is about hiding memory addresses: sort of true, but false in an important sense
I don't know if you're intentionally using "colour" to reference https://journal.stuffwithstuff.com/2015/02/01/what-color-is-... ? Cooperative multitasking (which I'd never heard of before) seems from its Wikipedia page to be primarily concerned with Operating System-level operations, whereas that article deals with programming language-level design. Or perhaps they are not distinct from one another in your perspective?
I ask because I've found `async/await` to just be an irritating overhead; a hoop you need to jump through in order to achieve what you clearly wanted to do all along. You write (pseudocode) `var foo = myFunction()`, and (depending on your language of choice) you either get a compilation or a runtime error reminding you that what you really meant was `var foo = await myFunction()`. By contrast, a design where every function is synchronous (which, I'd guess, more closely matches most people intuition) can implement async behaviour when (rarely) desired by explicitly passing function invocations to an Executor (e.g. https://www.digitalocean.com/community/tutorials/how-to-use-...). I'd be curious to hear what advantages I'm missing out on! Is it that async behaviour is desired more-often in other problem areas I don't work in, or that there's some efficiency provided by async/await that Executors cannot provide, or something else?
Then what you want are coroutines[1], which are strictly more flexible than async/await. Languages like Lua and Squirrel have coroutines. I and plenty of other people thing it's tragic that Python and Javascripts added async/await instead, but I assume the reason wasn't to make them easier to reason about, but rather to make them easier to implement without hacks in existing language interpreters not designed around them. Though Stackless Python is a CPython fork that adds real coroutines, also available as the greenlet module in standard CPython [2], amazing that it works.
[1] Real coroutines, not what Python calls "coroutines with async syntax". See also nearby comment about coroutines vs coop multitasking https://news.ycombinator.com/item?id=38859828
We used coroutines in our interrupt rich environment in our real time medical application way back when. This was all in assembly language and the coroutines vastly reduced our multithreading errors to effectively zero. This is one place where C , claimed to be close to the machine falls down.
how did coroutines reduce your multithreading errors
yes! i'm referencing that specific rant. except that what munificent sees as a disadvantage i see as an advantage
there's a lot of flexibility in systems design to move things between operating systems and programming languages. dan ingalls in 01981 takes an extreme position in 'design principles behind smalltalk' https://www.cs.virginia.edu/~evans/cs655/readings/smalltalk....
> An operating system is a collection of things that don't fit into a language. There shouldn't be one.
in the other direction, tymshare and key logic's operating system 'keykos' was largely designed, norm hardy said, with concepts from sigplan, the acm sig on programming languages, rather than sigsosp
sometimes irritating overhead hoops you need to jump through have the advantage of making your code easier to debug later. this is (i would argue, munificent would disagree) one of those times, and i'll explain the argument why below
in `var foo = await my_function()` usually if my_function is async that's because it can't compute foo immediately; the reasons in the examples in the tutorial you linked are making web requests (where you don't know the response code until the remote server sends it) and reading data from files (where you may have to wait on a disk or a networked fileserver). if all your functions are synchronous, you don't have threads, and you can't afford to tie up your entire program (or computer) waiting on the result, you have to do something like changing my_function to return a promise, and putting the code below the line `var foo = await my_function()` into a separate subroutine, probably a nested closure, which you pass to the promise's `then` method. this means you can't use structured control flow like statement sequencing and while loops to go through a series of such steps, the way you can with threads or async
so what if you use threads? the example you linked says to use threads! i think it's a widely accepted opinion now (though certainly not universal) that shared-mutable-memory threading is the wrong default, because race conditions in multithreaded programs with implicitly shared mutable memory are hard to detect and prevent, and also hard to debug. you need some kind of synchronization between the threads, and if you use semaphores or locks like most people do, you also get deadlocks, which are hard to prevent or statically detect but easy to debug once they happen
async/await guarantees you won't have deadlocks (because you don't have locks) and also makes race conditions much rarer and relatively easy to detect and prevent. mark s. miller, one of the main designers of recent versions of ecmascript, wrote his doctoral dissertation largely about this in 02006 http://www.erights.org/talks/thesis/index.html after several years working on an earlier programming language called e based on promises like the ones he later added to js; but i have to admit that, while i've read a lot of his previous work, i haven't read his dissertation yet
cooperative multitasking is in an in-between place; it often doesn't use locks and makes race conditions somewhat rarer and easier to detect and prevent than preemptive multitasking, because most functions you call are guaranteed not to yield control to another thread. you just have to remember which ones those are, and sometimes it changes even though your code didn't change
(in oberon, at least the versions i've read about, there was no way to yield control. you just had to finish executing and return, like in js in a web page before web workers, as i think i said upthread)
that's why i think it's better to have colored functions even though it sometimes requires annoying hoop-jumping
You will get them in .NET and C++, because they map to real threads being shared across tasks.
There is even a FAQ maintained by .NET team regarding gotchas like not calling ConfigureAwaitable with the right thread context in some cases where it needs to be explicitly configured, like a task moving between foreground and background threads.
Async only allows you to say “run foo now until it has data” to the JavaScript runtime.
IMO, async/await in JavaScript are more like one shot coroutines, not cooperative multitasking.
Having said that, the JavaScript event loop is doing cooperative multitasking (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Even...)
Is this the same as coroutines as in Knuth's TAOCP volume 1?
Sorry, my knowledge is weak in this area.
i could just be wrong tho
lua's coroutines aren't automatically scheduled (there isn't a built-in run queue) but explicitly resumed, which is a difference from the usual cooperative-multitasking systems; arguably on that basis you could claim that they aren't quite 'cooperative multitasking' on their own
the last time i implemented a simple round-robin scheduler for cooperative multitasking was in july, as an exercise, and it was in arm assembly language rather than lua. it was 32 machine instructions and 64 lines of code (http://canonical.org/~kragen/sw/dev3/monokokko.S), plus 14 lines of example code to run in the threads. when i went to go look at that just now i was hoping to come up with some kind of crisp statement about the relative importance or complexity of the stack-switching functionality and the run-queue maintenance facility, but in fact there isn't a clear separation between them, and that version of the code creates all the tasks at assembly time instead of runtime. a more flexible version with start, spawn, yield, and exit calls, which respects the eabi so you can write your task code in c (http://canonical.org/~kragen/sw/dev3/einkornix.h et seq.), is 53 lines of assembly and 34 machine instructions, but similarly has no real separation of the two concerns
Right, I think this is where I am coming from. Generators, for example, can be implemented via coroutines, but I would not call a generator "cooperative multitasking."
That's very cool! Yeah, I have never done this myself, but in my understanding implementations in assembly can be very small.
> when i went to go look at that just now i was hoping to come up with some kind of crisp statement about the relative importance or complexity of the stack-switching functionality and the run-queue maintenance facility, but in fact there isn't a clear separation between them
That's fair, but I don't think that's the final say here, as you were building a system for cooperative multitasking explicitly, with no reason to try and separate the concerns. When a system is very simple, there's much less reason for separation.
Actually, this makes me realize why I probably have this bias for thinking of them separately: async/await in Rust. The syntax purely creates a generator, it is totally inert. You have to bring along your own executor (which contains a scheduler among other things). Separating the two cleanly was an explicit design goal.
it certainly isn't the final say! it's just an analysis of how my own code turned out, not any kind of universal lesson
the implementation in monokokko, which reserves the r10 register to always point to the currently running task, is five instructions
.thumb_func
yield: push {r4-r9, r11, lr} @ save all callee-saved regs except r10
str sp, [r10], #4 @ save stack pointer in current task
ldr r10, [r10] @ load pointer to next task
ldr sp, [r10] @ switch to next task's stack
pop {r4-r9, r11, pc} @ return into yielded context there
interestingly, what you say of rust's generators is also sort of true of monokokko> The syntax purely creates a generator, it is totally inert. You have to bring along your own executor (which contains a scheduler among other things).
the above five instructions, or arguably just ldr r10, [r10], is the executor. the in-memory task object consists of the saved stack pointer, the link to the following task, and then whatever variables you have in thread-local storage. but from a different point of view you could say that the in-memory task object consists of the saved stack pointer, a pointer to executor-specific status information (which for this executor is the following task, or conceptually the linked list of all tasks), and then other thread-local variables. i think the difference if you were to implement this same executor with rust generators is just that you probably wouldn't make the linked list of all tasks an intrusive list?
> i think the difference if you were to implement this same executor with rust generators is just that you probably wouldn't make the linked list of all tasks an intrusive list?
You still could, and IIRC tokio uses an intrusive linked list to keep track of tasks. There's no specific requirements for how you keep track of tasks, or even a standardized API for executors, which is why you'll hear people talk about why they want "to be generic over runtimes" and similar.