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.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    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

    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, Kris Kafka, 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, Dmitry Farberov, 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, moon, Philipp Lenk, Hobsbawm, and Charles-Jianye Chen.

    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

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    Standard Seminars (English/German)

    Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    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 *