I'm trying to provide a meta-analysis of the implementation of async/await syntax amongst various of languages.
TL;DR: The behavior of async/await syntax itself does not mandate a eventloop driving the scheduling process at all. It can be also achieved by threads, generators (continuable closures) and state machines.
The misunderstanding of "async/await is backed by eventloop scheduling" starts with GUI applications (e.g. Android, Winform, Dartlang or even JavaScript that runs in browsers, you might even say Unity3D's monobehaviour lifecycle loop is driving the IEnumerator Unity coroutines).
There are tons of counterexamples: Kotlin is just better Java, it runs on JVM after all, and there is no builtin eventloop; Golang uses goroutine and channel which is a "logical blocking" waiting pattern; CSharp's async/await is merely a syntax sugar based on existing conventional thread model (see How Async/Await Really Works in C#), under the hood it is SynchronizationContext and ThreadPool, still no eventloop is found!
Let's first talk about builtin eventloops found in languages like JavaScript and Dart. I am not sure if they are in the spec, or just by implementation, but look at their environments: they were built for GUIs which is inherently single threaded, imagine in Android, everytime you want to edit some GUI element you have to Post them back to the main thread to avoid racing condition. One single main thread + builtin default eventloop can simplify the whole architecture design and implementation. The eventloop is like an engine, a driver of everything that's happeing in your program, where threading is basically non-existent and you use workers or "isolates" which is a lesser thread instead.
But back to Kotlin, CSharp and CPP, these languages are (at least designed to be) more general purposed and they cannot have a default eventloop that's hiding before you call Main function, they don't play that way. There is no single eventloop that's driving everything in the language even if in your system there has to be one. Every single thread drive their own thing. And async/await is just flattened, linearized CPS (Continuation Programming Style), implemented with a re-enterable generator state machine. The semantics of await
keyword in those languages are different than those who have eventloop builtin:
C++-ish be like: await means this “task” (not thread) logic stops here (in a way like a generator yield return), the whole following logic is stopped until the following Future is fulfilled, because the result here is needed for the logic to continue. The logic here can be resumed with another function call, in other words, the following logic is a continuation. the calculation or completion of this future may happen in a thread in the thread pool, or this current thread depending on your choice.
JavaScript-ish be like: await means our eventloop put away the current Task (a.k.a Promise or future) into the waiting queue, because it needs certain Futures to be fulfilled, and the loop will continue to process the tasks in the working queue. When the Future is fulfilled, this task will be put back into a pending queue that the eventloop will look at before it starts to process working queue tasks. It's more like a "queue play".
In other words, imagine the joint of running threads doing exactly the same thing as one single eventloop. At least I've dug through the source code of Python's asyncio, the whole point of asyncio is to utilize non-blocking IO like epoll, so that the main thread does not need to wait/block for anything, there is a eventloop and it just polls for IO socket ready state, check if anything is ready every "tick", then continue to execute the working queue tasks, like a pipeline. meanwhile if we use multithreading flavoured languages this polling must be done explicitly by a thread, blocking (then the new thread sleeps) or non-blocking (you have to poll), you can make your own eventloop also, which is literally a must for GUI related frameworks, just like what Android and Unreal Engine and Unity did.
co_wait
pushes into that loop. – Kovarco_await
with appropriate awaitable types. But this is ultimately a function of the asynchronous process you're waiting on, not of being an asynchronous process. That is, C++ doesn't have a default answer to what happens when youco_await
. Threads are the most common answer, but not the default. – Wattenberg