Why does C++'s async/await not need an event loop?

Viewed 8002

For someone who has some async experience from other languages (Python/JavaScript), when talking about async/await there is always an assumption that there is an event loop somewhere. But for C++ I have looked through the documentation and have not found anywhere talking about the event loop. Why is this the case?

For Node, it only has one default event loop. For Python you can create multiple ones if you want. But for C++ is this event loop assumed like Node? Or for some reason, do we not need it at all?

4 Answers

Python and JavaScript don't do CPU threads (well, Python can do threads, but that's not relevant here, because Python's thread stuff isn't inherently await-able). So they fake threading with "event loops".

C++ is a low-level language that knows what CPU threads are and expects to make use of them. As such, when you co_await on some expression, you are typically awaiting on a process that is (potentially) happening in another thread.

You certainly can fake threading with an event loop, and create some awaitable type that uses event loop processing. But that's not typically what C++ programmers do with asynchronous processing.

In C++ coroutines, the way the resumption of execution of a coroutine works depends entirely on the awaitable type you're using. It governs the scheduling of the function's resumption. Most awaitables in C++ will use some form of CPU threading (the most common method being to invoke the coroutine on the thread performing the async process it is waiting on). Others may have some kind of event loop or whatever. But the point is that this is a function of the thing which produces the value you're waiting on, not the coroutine itself.

Coroutines do not need an event loop.

When the computer reads co_await what happens is that it will jump to the function which called the coroutine and save all of its frame (the local variables values and so on).

The magic here is that the next time you call the coroutine you come back to this state. As you can see there is no need for event loop, only a place to store this frame.

There is no event loop in C++ and threads have very little to do with coroutine either.

When you co_await in C++, the execution of the function is suspended and the code continue to execute the caller, just as if the function had returned. In fact, it is implemented this way. co_await will change the internal state machine of the coroutine and will return.

The execution is resumed when the code explicitly resumes the function.

This is cooperative multitasking. Control of the execution is explicit and predictable.

Now, using most libraries you won't necessarily have to call back the coroutine to be resumed. This is where executors comes in. They are a bit like event loops but inside a library instead of baked in the language. User code can implement them as well, and you can have different one for different use cases. They will usually schedule the execution of coroutines and can also manage multiple threads to execute many of them at once.

For example, you could totally implement an executor on top of a thread pool. Large operation that wait on io won't need to block the thread for themselves, it will start the io operation and give back the thread to other tasks. Internally, the io operation will schedule the coroutine back into the thread pool to be resumed.

Another example would be io_uring on linux, which is the new async io api. One could wrap the facility with an executor and run io operation as coroutines. Technically, you don't need threads to do this one. Calls to co_await will simply schedule the io operation and the coroutine will resume once the kernel has enqueued a result.

Event loops are great for event driven applicaitons where you literally do nothing until some external event occurs. This was common for early X11 based programs that would display something and then waits for the operator to select or manipulate a widget, such as a slider bar or minimize widget, and the event would be passed to the c event handler. Later we setup up callbacks for events to make the event handler more streamlined. This is very common in IoT programming today.

Async processing is different. Async processing starts a thread to do something and all the process cares about is that it was done and possibly a return value. Basically it prevents the process from being blocked while an action is occurring.

Related