Starting Jobs with Coroutines

C++20 has three new keywords to make a coroutine out of a function: co_return, co_yield, and co_await. co_await requires an Awaitable as arguments and starts the Awaiter workflow. Let me show in this post what that means.

To understand this post, you should have a basic understanding of coroutines. Here are my previous posts to coroutines, presenting coroutines from the practical perspective.

co_return:

co_yield:

Before implementing Awaitables and showing their applications, I should write about the awaiter workflow.

The Awaiter Workflow

First, I have a short reminder. The awaiter workflow is based on the member functions of the Awaitable: await_ready(), await_suspend(), and await_resume(). C++20 has the two predefined Awaitables std::suspend_always and std::suspend_never, which I heavily used in this mini-series to coroutines.

  • std::suspend_always

struct suspend_always {
    constexpr bool await_ready() const noexcept { return false; }
    constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
    constexpr void await_resume() const noexcept {}
};
  • std::suspend_never

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    struct suspend_never {
        constexpr bool await_ready() const noexcept { return true; }
        constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
        constexpr void await_resume() const noexcept {}
    };
    

    Here is the awaiter workflow in prose.

    awaitable.await_ready() returns false:                   // (1)
        
        suspend coroutine
    	
        awaitable.await_suspend(coroutineHandle) returns:    // (3)
    	
            void:                                            // (4)
                awaitable.await_suspend(coroutineHandle);
                coroutine keeps suspended
                return to caller
    
            bool:                                            // (5)
                bool result = awaitable.await_suspend(coroutineHandle);
                if result: 
                    coroutine keep suspended
                    return to caller
                else: 
                    go to resumptionPoint
    
            another coroutine handle:	                 // (6)
                auto anotherCoroutineHandle = awaitable.await_suspend(coroutineHandle);
                anotherCoroutineHandle.resume();
                return to caller
    	
    resumptionPoint:
    
    return awaitable.await_resume();                         // (2)
    

    The workflow is only executed if awaitable.await_ready() returns false (line 1). In case it returns true, the coroutine is ready and returns with the result of the call awaitable.await_resume() (line 2).

    Let me assume that awaitable.await_ready() returns false. First, the coroutine is suspended (line 3), and the return value is immediately evaluated. The return type can be void (line 4), a boolean (line 5), or another coroutine handle (line 6), such as anotherCoroutineHandle. Depending on the return type, the program flow returns, or another coroutine is executed.

    awaiterWorkflow

    Let me apply the theory and start a job on request.

    Starting a Job on Request

    The coroutine in the following example is as simple as it can be. It awaits on the predefined Awaitable std::suspend_never().

    // startJob.cpp
    
    #include <coroutine>
    #include <iostream>
     
    struct Job { 
        struct promise_type;
        using handle_type = std::coroutine_handle<promise_type>;
        handle_type coro;
        Job(handle_type h): coro(h){}
        ~Job() {
            if ( coro ) coro.destroy();
        }
        void start() {
            coro.resume();                                    // (6) 
        }
    
    
        struct promise_type {
            auto get_return_object() { 
                return Job{handle_type::from_promise(*this)};
            }
            std::suspend_always initial_suspend() {           // (4)
                std::cout << "    Preparing job" << '\n';
                return {}; 
            }
            std::suspend_always final_suspend() noexcept {    // (7)
                std::cout << "    Performing job" << '\n'; 
                return {}; 
            }
            void return_void() {}
            void unhandled_exception() {}
        
        };
    };
     
    Job prepareJob() {                                        // (1)
        co_await std::suspend_never();                        // (2)
    }
     
    int main() {
    
        std::cout <<  "Before job" << '\n';
    
        auto job = prepareJob();                              // (3)                       
        job.start();                                          // (5)  
    
        std::cout <<  "After job" <<  '\n';
    
    }
    

    You may think that the coroutine prepareJob (line 1) is meaningless because the Awaitable always suspends. No! The function prepareJob is at least a coroutine factory using co_await (line 2) and returning a coroutine object. The function call prepareJob() in line 3 creates the coroutine object of type Job. When you study the data type Job, you recognize that the coroutine object is immediately suspended because the member function of the promise returns the Awaitable std::suspend_always (line 5). This is precisely why the function call job.start (line 5) is necessary to resume the coroutine (line 6). The member function final_suspend() also returns std::suspend_always (line 27).

    startJob

    The program startJob.cpp is an ideal starting point for further experiments. First, making the workflow transparent eases its understanding.

    The Transparent Awaiter Workflow

    I added a few comments to the previous program.

    // startJobWithComments.cpp
    
    #include <coroutine>
    #include <iostream>
    
    struct MySuspendAlways {                                  // (1)
        bool await_ready() const noexcept { 
            std::cout << "        MySuspendAlways::await_ready"  << '\n';
            return false; 
        }
        void await_suspend(std::coroutine_handle<>) const noexcept {
            std::cout << "        MySuspendAlways::await_suspend"  << '\n';
    
        }
        void await_resume() const noexcept {
            std::cout << "        MySuspendAlways::await_resume"  << '\n';
        }
    };
    
    struct MySuspendNever {                                  // (2)
        bool await_ready() const noexcept { 
            std::cout << "        MySuspendNever::await_ready"  << '\n';
            return true; 
        }
        void await_suspend(std::coroutine_handle<>) const noexcept {
            std::cout << "        MySuspendNever::await_suspend"  << '\n';
    
        }
        void await_resume() const noexcept {
            std::cout << "        MySuspendNever::await_resume"  << '\n';
        }
    };
     
    struct Job { 
        struct promise_type;
        using handle_type = std::coroutine_handle<promise_type>;
        handle_type coro;
        Job(handle_type h): coro(h){}
        ~Job() {
            if ( coro ) coro.destroy();
        }
        void start() {
            coro.resume();
        }
    
    
        struct promise_type {
            auto get_return_object() { 
                return Job{handle_type::from_promise(*this)};
            }
            MySuspendAlways initial_suspend() {         // (3)
                std::cout << "    Job prepared" << '\n';
                return {}; 
            }
            MySuspendAlways final_suspend() noexcept {  // (4)
                std::cout << "    Job finished" << '\n'; 
                return {}; 
            }
            void return_void() {}
            void unhandled_exception() {}
        
        };
    };
     
    Job prepareJob() {
        co_await MySuspendNever();                     // (5)
    }
     
    int main() {
    
        std::cout <<  "Before job" << '\n';
    
        auto job = prepareJob();                      // (6)
        job.start();                                  // (7)
    
        std::cout <<  "After job" <<  '\n';
    
    }
    

    First, I replaced the predefined Awaitables std::suspend_always and std::suspend_never with Awaitables MySuspendAlways (line 1) and MySuspendNever (line 2). I use them in lines 3, 4, and 5. The Awaitables mimic the behavior of the predefined Awaitables but additionally write a comment. Due to the use of std::cout, the member functions await_ready, await_suspend, and await_resume cannot be declared as constexpr.

    The screenshot of the program execution shows the control flow nicely, which you can observe on the Compiler Explorer.

    startJobWithComments

    The function initial_suspend (line 3) is executed at the beginning of the coroutine and the function final_suspend at its end (line 4). The call prepareJob() (line 6) triggers the creation of the coroutine object, and the function call job.start() its resumption and, hence, completion (line 7). Consequently, the members await_ready, await_suspend, and await_resume of MySuspendAlways are executed. When you don’t resume the Awaitable, such as the coroutine object returned by the member function final_suspend, the function await_resume is not processed. In contrast, the Awaitable’s MySuspendNever the function is immediately ready because await_ready returns true and, hence, does not suspend.

    Thanks to the comments, you should have an elementary understanding of the awaiter workflow. Now, it’s time to vary it.

    What’s next?

    In my next posts,  I automatically resume the Awaiter on the same and, finally, on a separate thread.


    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *