Mircea Baja - 24 May 2025 # Lazy task
foo
bar
10s
stack
foo frame
bar frame
--- # Motivation --- # Goal ```cpp task
echo(socket s) { buffer buff; while (true) { std::size_t length = co_await async_read_some(s, buff); co_await async_write(s, buff, length); } } ``` - easy to understand behaviour e.g.: - scope for the buffer - exceptions (but slow, so use sparingly) - cancellation --- # Basic example ```cpp task
bar() { co_await async_sleep(10s); } task
foo() { co_await bar(); } void somewhere() { // run foo() // also resume sleeping coroutines } ``` --- # Basic example
foo
bar
10s
--- # Basic example - in foo
foo
stack
foo frame
--- # Basic example - in bar *
foo
bar
stack
foo frame
bar frame
--- # Basic example - sleep - wakeup
foo
bar
10s
stack
foo frame
bar frame
--- # Basic example - back in foo
foo
bar
10s
stack
foo frame
--- # Code --- # Promise base ```cpp template
class promise_base { std::variant
result_; public: template
requires std::convertible_to
void return_value(U && x) noexcept(std::is_nothrow_constructible_v
) { assert(0 == result_.index()); result_.template emplace<1>(std::forward
(x)); } void unhandled_exception() noexcept { assert(0 == result_.index()); result_.template emplace<2>(std::current_exception()); } ``` --- # Promise base ```cpp T get_result() const { switch(result_.index()) { case 1: return std::move(std::get<1>(result_)); case 2: std::rethrow_exception(std::get<2>(result_)); default: std::terminate(); } } }; // promise_base ``` --- # Promise base - void ```cpp template<> class promise_base
{ std::exception_ptr exception_{}; public: void return_void() noexcept { } void unhandled_exception() noexcept { assert(nullptr == exception_); exception_ = std::current_exception(); } void get_result() const { if (exception_) { std::rethrow_exception(exception_); } } std::exception_ptr get_result_exception() const noexcept { return exception_; } }; ``` --- # Promise ```cpp template
class [[nodiscard]] task { public: class promise_type : public promise_base
{ friend task; std::coroutine_handle<> parent_coro_; public: promise_type() noexcept = default; promise_type(const promise_type&) = delete; promise_type& operator=(const promise_type&) = delete; task get_return_object() noexcept { return { std::coroutine_handle
::from_promise(*this) }; } ``` --- # Promise - lazy ```cpp std::suspend_always initial_suspend() noexcept { return {}; } struct final_awaiter { [[nodiscard]] constexpr bool await_ready() const noexcept { return false; } std::coroutine_handle<> await_suspend( std::coroutine_handle
child_coro) noexcept { return child_coro.promise().parent_coro_; } [[noreturn]] constexpr void await_resume() const noexcept { std::unreachable(); //std::terminate(); } }; // final_awaiter final_awaiter final_suspend() noexcept { return {}; } }; // promise_type ``` --- # Task ```cpp private: unique_coroutine_handle
unique_child_coro_; task(std::coroutine_handle
child_coro) noexcept : unique_child_coro_{ child_coro } { } public: task(const co&) = delete; task& operator=(const co&) = delete; ``` --- # Task awaiter ```cpp [[nodiscard]] constexpr bool await_ready() const noexcept { return false; } std::coroutine_handle<> await_suspend( std::coroutine_handle<> parent_coro) noexcept { std::coroutine_handle
child_coro = unique_child_coro_.get(); child_coro.promise().parent_coro_ = parent_coro; return child_coro; } T await_resume() const { return unique_child_coro_.get().promise().get_result(); } }; // task ``` --- # Discussion --- # History This is close to the task/lazy [P1056r1 proposal](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1056r1.html) which did not make it. --- # Q: How do we start one? - e.g. in our simple exemple how do we start `foo()`? - the problem is that we need to `co_await` in order to have a coroutine handle, and we can only `co_await` in a coroutine - A: the answer is long, will explain another time --- # Bad: no cancellation - needs major changes to support cancellation --- # Good: lazy Bad example: `future.then` is concurrent with work being done ```cpp future
f = get_future_for_async_activity(); future
f2 = f.then([](int i) { return std::to_string(i); }); std::cout << f2.get() << '\n'; ``` - setting "continuation" (the lambda specified in `then`) requires synchronization - also Q: What to do in final_suspend for eager tasks if the parent can't be resumed becaus it did not suspend? A: more synchronization --- # Good: lazy Another reason is (correct) error handling becomes impractical for anything but the most trivial examples of eager work see [Structured Concurrency: Writing Safer Concurrent Code with Coroutines... - Lewis Baker - CppCon 2019](https://www.youtube.com/watch?v=1Wy5sq3s2rg) ```cpp future f1 = eager_async_work1(); bool joined_f1 = false; std::exception_ptr ex; try { co_await eager_async_work2(); joined_f1 = true; co_await std::move(f1); co_return; } catch(...) { ex = current_exception(); } if (!joined_f1) co_await std::move(f1); std::rethrow_exception(ex); ``` --- # Bad: Scope for bugs - same as for the std::generator ```cpp task
foo() { task
bar("alpha"); co_await bar; } ``` -- ```cpp task
bar(const std::string& letter) { std::cout << letter << '\n'; co_return; } ``` --- # The move trick Used by Lewis Baker's coro library, also see [Raymond Chen: Making it impossible to co_await a task twice](https://devblogs.microsoft.com/oldnewthing/20210414-00/?p=105095) ```cpp template
class [[nodiscard]] task { // ... task_awaiter operator co_await() && { return {std::move(unique_child_coro_}; } }; ``` Meant to prevent ```cpp task
foo() { task
bar("alpha"); co_await bar; co_await bar; } ``` **Clever, but not a good solution for things that are meant to be done only once**. In particular if std::move is used in both places it compiles, but is incorrect code. Also it still has the reference argument bug. --- # Questions?