Summary
I'm now convinced that async/await is, in fact, a bad abstraction for most languages. We should be aiming for something better instead.
The next generation of computers will be able to send messages to each other via the internet. The next generation will also be able communicate via the Internet.
Functional languages and imperativelanguages are dealing with ‘waiting’. Both of the functions (for the cat and the mouse) can be seen as separate functions. When the code calls sleep(10) there's an expectation by the programmer that the computer will temporarily pause theexecution and continue later.
In the original blocking code, when we invoked sleep we suspended for 10milliseconds implicitly; we cannot do the same with the async call. This is the crucial aspect of why we're having these “colored functions”
Contract wise, there is absolutely nothing that says one has to call resolve. The anonymous function in the promise itself will return, the stack will unwind, and we are left with a “pending” promise that will eventually get garbage collected.
The problem is that we can't get rid of the tokens. We have to use them to make new ones. So we have to keep the old ones.
Python's promise model means that you need extra affordances to stitch these calls together into call stacks. This all creates extra problems that did not exist before. The biggest footgun in Pythonync remains that write is non blocking.
There is no “await touching this memory’ expression, because if there were, we would have to await everywhere. If you're touching a page that hasn't been loaded yet, the operating system will have to shovel it into memory before returning back to you.
In a traditional imperative language based on threads, the act of spawning a thread is usually hidden behind a (often convoluted) standard standard. This is where I will have to concede thatync/await has something going for it. It moves the reality of current execution right into the language.
We need a way to say: run this concurrently, but don't wait for it to return. At the end of the day, we don't necessarily have to care about it as developer if the language gets it right.
Threads are so great, why all that talking aboutcoroutines and promises that underpins so much of 'async/await' in different languages?
JavaScript has no threads. Python on the other hand had a completely different origin story. In the days before async/await, Python already had threads.
Stackless did not have a bright future because the way it worked meant that you could not have interleaving Python -> C calls and suspend with them on the stack. There was a second attempt in Python called “greenlet’ which was implementing coroutines in a custom extension module. However, like stackless, that did not win out. Instead, the generator system that Python had for years was gradually upgraded into a coroutine system with syntax support.
In C# there are threads; there is syntax to yield. A coroutine inPython will also start out with not running, unlike in JavaScript where it's effectively always scheduled.
Rust's system is probably the weirdest of them all because it's polling-based. Unless you actively "wait" for a task to complete, it will not make progress. That is why at least at one point what C# did was just to splice functions into chained closures.
The reason I wanted you to understand all this is that all these differentlanguages share the same syntax, yet what you can do with it is completelydifferent. What they all have in common is that async functions can only be called by the scheduler.
Coroutines are an important building block, and if any future language is looking at this post: you should put them in. But coroutine should be very lightweight, and they can be abused.
I want to follow up on an another blog post about what is needed to make virtualthreads easier to work with. The biggest innovation here goes to Trio, which introduced the concept of structured concurrency. That concept has eventuallyfound a home even in asyncio with the concept.
Structured concurrrency needs to become a thing in a threaded world. Every thread or task has a clear beginning and end, which makes it easier to follow what each thread is doing.
If we take a step back, it seems pretty clear to me that we have veered off course by adoptingync/await in languages that have realthreads. Future language design should rethink concurrency once more.
I don't think actor frameworks are the right fit for this application. A combination of structured concurrency, channels and syntax support would be better. Watch this space for a future blog post on this topic.