coroutine in C++
In this article, we will discuss the coroutine in C++ with its use, syntax, and examples.
What is coroutine?
A coroutine is a particular function in C++ that allows the creation of asynchronous code sequentially and uncomplicatedly. It allows you to stop and start the execution of a function at particular locations without stopping the execution of the entire program. It makes it simpler to create asynchronous, more readable, and manageable code, such as code that performs I/O operations or waits for events.
Note:
Variadic arguments, simple return statements, or placeholder return types (auto or Concept) are prohibited in coroutines.
Coroutines are not used for constexpr, conssteval, constructors, destructors, or the primary function.
Essential Terminology:
There are several essential terminology in coroutine function. Some of them are as follows:
- Cooperative multitasking, also known as non-preemptive multitasking: It occurs when a process or thread participating in multitasking periodically gives up control of itself or becomes idle or logically stopped. This type of multitasking is referred to as "cooperative" because all programs must work together for the overall scheduling system to function.
- Subroutine: A subroutine is any ordinary function you write.
With the addition of the coroutine<> header and associated keywords and functions, coroutines are available in C++ as of C++ 20. It would be best if you did the following actions to define a coroutine function:
Define the coroutine function: A coroutine function is a function that uses the co_await keyword and returns a special type known as coroutine_handle<> or any type that models the coroutine_handle<> concept. The function signature looks like this:
( )
Use co_await: The co_await keyword allows you to asynchronously wait for the outcome of another coroutine or awaitable object inside the coroutine function. The coroutine can pause its execution until the awaited operation is finished, thanks to the co_await expression.
Example:
#include#include #include #include using namespace std; auto switch_to_new_thread(thread& out) { struct awaitable { jthread* p_out; bool await_ready() { return false; } void await_suspend(coroutine_handle<> h) { jthread& out = *p_out; if (out.joinable()) throw runtime_error("Output jthread parameter not empty"); out =jthread([h] { h.resume(); }); // Potential undefined behaviour: accessing potentially destroyed *this // cout << "New thread ID: " << p_out->get_id() << ' '; cout << "New thread ID: " << out.get_id() << ' '; // this isOK } void await_resume() {} }; return awaitable{&out}; } struct task { struct promise_type { task get_return_object() { return {}; } suspend_never initial_suspend() { return {}; } suspend_never final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; }; task resuming_on_new_thread(jthread& out) { cout << "Coroutine started on thread: " << this_thread::get()<< ' '; co_await switch_to_new_thread(out); // awaiter destroyed here cout << "Coroutine resumed on thread: " << this_thread::get_id() << ' '; } int main() { jthread out; resuming_on_new_thread(out); }
Output:
![coroutine in C++](https://static.tutorialandexample.com/cpp/coroutine-in-cpp1.png)
Awaitable items are what: Any object that offers the three functions await_ready(), await_suspend(), and await_resume() is considered an "awaitable" object. When the coroutine comes into contact with the co_await statement, these functions specify how it should behave. They must decide whether to suspend the coroutine, where to save its state, and how to restore it.
Use co_return: You can use the co_return keyword and the value you want to return to return a value from a coroutine.
Execution pattern using a coroutine:
There are two things connected to a C++ coroutine. The promise object is the first, and the coroutine object is the second. A promise object is an object that gives a synchronization point and can contain a value that can be obtained by any future a=object (often another thread). Additionally, a heap is used with the coroutine state when made available efficiently. The following characteristics apply to the state object:
- The available parameters.
- The local and temporary variables have a limited time stamp and scope up until the present suspension point.
- Promise objects for manipulation within.
- We represent the correct state and value of the local variables to know where to resume and continue the execution.
coroutine handles:
We use the coroutine handles to build and execute code outside the coroutine frame. We employ the non-use handling handlers of the coroutine hold to resume the coroutine's execution and simultaneously remove the C++ coroutines from the frame. A C++ coroutine handle is comparable to a c pointer, which can be easily copied, but we lack a destructor to clear the memory it has taken up. Therefore, to terminate the coroutine and prevent memory leaking, we must utilize the coroutine_handle:: destroy method. The coroutine handler pointing to the deleted coroutine is removed when the coroutine is destroyed. Now that it points to a garbage value, the coroutine handler will no longer function when called.
Coroutine return object:
The C++ coroutine returns an object with a nested type::promise_type as its return type. A method called r get_return_object, which returns an outer object of type r, should be included in the r::promise_type. A coroutine function is the result of the get_return_object function.
The promise object:
A promise-type instance exists in the coroutine state. We can add a value field to transfer the values from the coroutine to the primary function. The coroutine handle is kept as coroutine_handleReturnObject::promise_type> rather than converted to a coroutine_handle<>.
The co_yield operator:
The co_yield operator returns the value it yielded from the expression to the caller. It is one of the most popular components of the resumable generator functions, it suspends the running coroutine.
Example:
#include#include #include #include using namespace std; template struct Generator { // The class name 'Generator' is our choice, and it is not required for coroutine // magic. The compiler recognizes the coroutine by the presence of the 'co_yield' keyword. // You can use the name 'MyGenerator' (or any other name) instead as long as you include // nested struct promise_type with 'MyGenerator get_return_object()' method. struct promise_type; using handle_type = coroutine_handle ; struct promise_type // required { T value_; exception_ptr exception_; Generator get_return_object() { return Generator(handle_type::from_promise(*this)); } suspend_always initial_suspend() { return {}; } suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() { exception_ = current_exception(); } // saving // exception template From> // C++20 concept suspend_always yield_value(From&& from) { value_ = std::forward (from); // caching the result in promise return {}; } void return_void() { } }; handle_type h_; Generator(handle_type h): h_(h) { } ~Generator() { h_.destroy(); } explicit operator bool() { fill(); // The only way to reliably find out whether or not we finished coroutine, // whether or not there is going to be a next value generated (co_yield) // in coroutine via C++ getter (operator () below) is to execute/resume // coroutine until the next co_yield point (or let it fall off the end). // Then we store/cache the result in the promise to allow the getter operator() below // to grab it without executing the coroutine). return !h_.done(); } T operator()() { fill(); full_ = false; // we are going to move out previously cached // result to make promise empty again return std::move(h_.promise().value_); } private: bool full_ = false; void fill() { if (!full_) { h_(); if (h_.promise().exception_) rethrow_exception(h_.promise().exception_); // propagate coroutine exception in called context full_ = true; } } }; Generator fibonacci_sequence(unsigned n) { if (n == 0) co_return; if (n > 94) throw runtime_error("Too big Fibonacci sequence. Elements would overflow."); co_yield 0; if (n == 1) co_return; co_yield 1; if (n == 2) co_return; uint64_t a = 0; uint64_t b = 1; for (unsigned i = 2; i < n; i++) { uint64_t s = a + b; co_yield s; a = b; b = s; } } int main() { try { auto gen = fibonacci_sequence(10); // max 94 before uint64_t overflows for (int j = 0; gen; j++) cout << "fib(" << j << ")=" << gen() << ' '; } catch (const std::exception& ex) { cerr << "Exception: " << ex.what() << ' '; } catch (...) { std::cerr << "Unknown exception. "; } }
Output:
![coroutine in C++](https://static.tutorialandexample.com/cpp/coroutine-in-cpp2.png)
The co_return operator:
A coroutine comes to a close when the co_return operator is used. Signalling can be done in one of three ways.
- The value e can be returned using Co_return e.
- The co_routine can utilize the co_return operator to signify the conclusion of a coroutine without a final value.
- Much like the preceding point, we may use the co_return operator to fall off the end of the function.
Coroutines vs Subroutines:
There are several differences between the coroutines and subroutines. Some of the main differences are as follows:
- With subroutines, execution starts at the beginning and ends at the end.
- Coroutines have a particular case known as a subroutine. Any subroutine that is not called 'yield' (relinquishing control) can be converted to a coroutine.
- Subroutines don't hold the entire state between calls and return once.
- Coroutines can quit by invoking other coroutines, which may then return to the place in the original coroutine where they were called. From the perspective of the coroutine, it is calling another coroutine rather than genuinely exciting.
- An instance of a coroutine maintains state and changes between calls.
![coroutine in C++](https://static.tutorialandexample.com/cpp/coroutine-in-cpp3.png)
Applications of coroutine in C++:
Asynchronous programming can be made easier and more efficient in C++ using coroutines in various applications. Coroutines are commonly used in the following C++ applications:
Game Development: In game development, coroutines can help handle animations, AI behaviour, and game loop control in a more organized way.
State Machines: Coroutines can make it easier to design state machines, allowing for a more organized and readable representation of the various states and transitions.
Web Servers and Microservices: Coroutines can make handling several concurrent client connections easier and manage asynchronous communication in web servers and microservices.
Generators: You can iterate over a possibly infinite or lazy sequence using coroutines to build generator functions that deliver values one at a time.
Asynchronous testing: Using coroutines, asynchronous test cases can be written in a form that is easier to read and understand, improving the testing of asynchronous programs.
Asynchronous I/O: Without complicated callback methods, coroutines can manage asynchronous I/O operations, including file reading and writing, network connectivity, and waiting for user input.
Parallel Processing: Coroutines can provide parallel processing and lightweight concurrent jobs. Since coroutines don't require explicit thread management, several coroutines can operate simultaneously.
Resource Management: Coroutines can be used when allocating and releasing resources asynchronously, such as when managing file locks or database connections.
Asynchronous Tasks: Coroutines make it simpler to work with asynchronous processes, such as requesting information from a distant server or watching for several tasks to finish at once.
Asynchronous GUI Applications: Coroutines can be used in graphical user interfaces (GUI) to handle asynchronous actions and events, such as user input, animations, and background processes.
Compared with standard callback-based or thread-based approaches, coroutines in C++ offer a more natural and linear way to express asynchronous behaviour, resulting in more manageable and legible code. To properly use coroutines, it is necessary to understand their principles and how to use them.