April 22nd, 2021

C++ coroutines: Improving cold-start coroutines which complete synchronously

Last time, we learned that the naïve implementation of cold-start coroutines is susceptible to stack build-up. What we want is for await_suspend to return false if the coroutine completed synchronously. This is tricky because that reintroduces a race condition where the coroutine runs asynchronously and completes at the same time we try to transition from synchronous to asynchronous behavior in the awaiter.

For example, we could try this:

        auto final_suspend() noexcept
        {
            struct awaiter : std::experimental::suspend_always
            {
                simple_promise_base& self;
                void await_suspend(
                    std::experimental::coroutine_handle<>)
                    const noexcept
                {
                    if (self.m_waiting()) self.m_waiting();
                }
            };
            return awaiter{ {}, *this };
        }

        auto client_await_suspend(
            std::experimental::coroutine_handle<> handle)
        {
            as_handle().resume();
            m_waiting = handle;
            return m_holder.is_empty();
        }

The idea here is that we don’t register a resumption coroutine handle immediately. Instead, we let the coroutine resume, and if it completes synchronously, its final_suspend resumes the awaiter if one exists. In the case of synchronous completion, there won’t be an awaiter yet. Back in the awaiter, we register the awaiting coroutine for resumption after the synchronous portion of the awaited-for coroutine finishes, and then we peek at whether it indeed completed synchronously. If so, then we return false.

This algorithm doesn’t work because the coroutine may have continued asynchronously, and the asynchronous completion creates a data race against the awaiter trying to check whether the coroutine completed synchronously.

We’ll have to bring back the atomic waiter.¹

Return to the version where m_waiting was a std::atomic<void*>.

        auto final_suspend() noexcept
        {
            struct awaiter : std::experimental::suspend_always
            {
                simple_promise_base& self;
                void await_suspend(
                    std::experimental::coroutine_handle<>)
                    const noexcept
                {
                    auto waiter = self.m_waiting.exchange(completed_ptr,
                        std::memory_order_acq_rel);
                    if (waiting != abandoned_ptr) self.destroy();
                    else if (waiting != running_ptr) std::experimental::
                        coroutine_handle<>::from_address(waiting).resume();
                }
            };
            return awaiter{ {}, *this };
        }

        auto client_await_suspend(
            std::experimental::coroutine_handle<> handle)
        {
            as_handle().resume();
            return m_waiting.exchange(handle.address(),
                std::memory_order_acq_rel) == running_ptr;
        }

We now have implementations for both cold-start and hot-start coroutines. Next time, we’ll unify them by using another coroutine trick.

¹ “Atomic Waiter” sounds like a failed superhero from the 1950’s.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

3 comments

Discussion is closed. Login to edit/delete existing comments.

  • Christopher Davidson

    Should line 10 be:

    if (self.m_waiting) self.m_waiting();

    that is, no parens in the if test?

  • 紅樓鍮

    Congratulations for the 40th consecutive article on coroutines!

    • Ivan K

      I like it