A coroutine is a function that can be suspended during execution and resumed at a later stage. In C++20, when any of the co_await, co_yield, co_return appears inside a function, the function is a coroutine.

A simple sample code for a C++20 coroutine:

number_generator and co_yield appear in the co_return so this is not an ordinary function, but a coroutine whenever the program executes to line 4 co_yield i; The coroutine suspends and control of the program returns to the caller until the caller calls the resume method, at which point it reverts to where the last coroutine yielded and continues execution.

number_generator return type is coro_ret, and the coroutine itself does not return this type of data through return, which is a key point in C++20 to implement coroutine: in the return type T of the coroutine, there must be T::p romise_type This type is defined, and this type implements several interfaces. Or look at the code first:

coro_ret get_return_object() This interface should be able to construct the return value of a coroutine with its own instance of promise, which will be called before the coroutine is running, and the return value of this interface will be used as the return value of the coroutine.

awaiter initial_suspend() This interface will be called before the coroutine is created (that is, the first call), before it is really run, if the interface returns std::suspend_never{}, then as soon as the coroutine is created, it will be executed immediately; If std::suspend_always{} is returned, then when the coroutine is created, it will be in a suspended state, will not be executed immediately, and the first execution will be triggered only if the caller actively resumes. Both of these values are actually awaiter types, which will be explained later.

awaiter yield_value(T v) This interface will be called when co_yield v, the value v followed by the co_yield is passed as a parameter, here is generally to save this value, provide it to the caller of the coroutine, the return value is also awaiter, here generally return std::suspend_always{}.

void return_value(T v) This interface will be called when co_return v, and the value v followed by the co_return will be passed as an argument, which is generally saved and provided to the coroutine caller.

void return_void() This interface is called if no value is followed by the co_return. return_void and return_value can only select one implementation, otherwise a compilation error will be reported.

awaiter final_suspend() interface called after the coroutine finally exits, if std::suspend_always is returned, the user needs to call the coroutine_handle destroyer interface to release the resources related to the coroutine; If std::suspend_never then after the coroutine ends, the handle corresponding to the coroutine is already empty, and destroy can no longer be called.

void unhandled_exception() This interface is called if the code in the coroutine throws an exception.

It can be seen that the work of the promise class is mainly two: one is to define the execution process of the coroutine, the main interface is the initial_suspend, the final_suspend, and the other is responsible for the data transfer between the coroutine and the caller, the main interface is the yield_value and the return_value.

std::coroutine_handle is the control handle class of the coroutine, the most important interface is promise, resume, the former can get the promise object of the coroutine, the latter can restore the operation of the coroutine. In addition, there is a destroy interface to destroy a coroutine instance, and a done interface to return whether the coroutine has ended running. Through the std::coroutine_handle::from_promise() method, you can get the corresponding handle from the promise instance.

Several other interfaces in the coro_ret resume, done, and get_data are not required, but exist just for ease of use.

To sum up, a coroutine is associated with these objects:

promise

coroutine handle

coroutine state

This is an internal object allocated on the heap, not exposed to the developer, and is used to save the relevant data and state within the coroutine, specifically:

The promise object

The parameters passed to the coroutine

The data related to the current hang starting point

The lifecycle spans temporary and local variables that are hanging from the beginning, that is, variables that need to be recovered after resume.

To implement a coroutine in C++20, you need to define the return type T of a coroutine, and within this T you need to define a promise_type type, which implements several specified interfaces, which is sufficient. Thus, to develop a coroutine that contains asynchronous operations, the structure of the code would look something like this:

It can be seen that within the coroutine, initiating an asynchronous operation and getting the result is split into two steps by yield, and there is still a clear difference between synchronous code. At this point, co_await can play its role, and the coroutine code after using co_await will be like this

This is basically no different from the synchronization code, except for this co_await

co_await most commonly used is auto ret=co_await expr, co_await followed by an expression, the execution of the entire statement has a variety of situations, is more complicated. The simplified version described here is mainly simplified to simplify the role of promise.await_transform, as well as the awaitable object, you can click on the link below to see the full description. It is assumed here that the promise_type of coroutines does not implement await_transform method. 

https://en.cppreference.com/w/cpp/language/coroutines

In code, it goes like this:

The first is expr evaluation

The return value type (awaiter) of the expr expression must implement these interfaces: await_ready, await_suspend, and await_resume.

await_ready is called, if it returns true, then the coroutine will not be suspended at all, and will directly call the await_resume() interface, take this interface as the return value of await, and continue to execute the coroutine.

If the await_ready returns false, the coroutine is suspended, and then the await_suspend interface is called, and the handle of the coroutine is passed to this interface. Note that the coroutine is suspended at this point, but control has not yet been given to the caller.

If the return type of the await_suspend interface is void, or the return type is bool and the return value is true, then control is returned to the caller.

If the await_suspend interface returns false, then the coroutine is resumed, and then the await_resume is called, taking this interface as the return value of await and continuing to execute the coroutine.

If the coroutine is suspended in the previous steps, then when the coroutine caller resumes, the await_resume interface will be called first, and this interface will be used as the return value of await to continue executing the coroutine.

Taking the connect operation that encapsulates a socket as an example, we hope to connect a tcp address in the coroutine like this:

So what needs to be done is

The client.connect in line 5 first initiates an asynchronous connection request (set the socket to noneblock, then connect, and adds the socket and its own pointer to the epoll), and the type returned needs to be an awaiter, that is, to implement the three interfaces: await_ready, await_suspend, and await_resume

In await_ready, to determine whether the connection has been established (in some cases connect will return successfully immediately), or if something goes wrong (such as passing an illegal argument to connect), it needs to return true at this time, and the coroutine will not hang at all. In other cases, you need to return false to hang the coroutine

In await_suspend, you can save the incoming coroutine handle and return it directly to true.

In the await_resume, the result of the next connection is determined, 0 is returned successfully, and the error code is returned in other cases.

In the main loop outside the coroutine, use the epoll for polling, when the corresponding handle has an event (successful connection, timeout, error), take out the corresponding client pointer, set the result of the connection, and resume the coroutine.

The approximate code is as follows:

After understanding the co_await, you can look back at the previous content, std::suspend_never and std:::suspend_always appear many times before are two predefined awaiters, there are also definitions of the three interfaces, interested students can look at the corresponding source code. The initial_suspend, final_suspend, yield_value of the promise object are all awaiters, and the system actually performs co_await promise.initial_suspend() co_yield actually executes co_await promise.yield_value(). If needed, you can also return a custom awaiter.

 About the author

Yang Liangcong

Tencent back-office development engineer

Tencent back-end development engineer, graduated from Huazhong University of Science and Technology, is currently responsible for the back-end development of Happy Bucket Landlord, and has rich back-end development experience.

 Recommended reading