Starting Jobs with Coroutines

Contents[Show]

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.

TimelineCpp20

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 I implement Awaitables and show 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

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 immediately the return value of awaitable.await_suspend() is 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 exactly the reason 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 directly 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 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, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang Gärtner,  Louis St-Amour, Stephan Roslen, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Avi Kohn, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Tobi Heideman, Daniel Hufschläger, Red Trip, Alexander Schwarz, and Tornike Porchxidze.

 

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, and Richard Sargeant.

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

Seminars

I'm happy to give online-seminars or face-to-face seminars world-wide. Please call me if you have any questions.

Bookable (Online)

Deutsch

English

Standard Seminars 

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

New

Contact Me

Modernes C++,

RainerGrimmSmall

 


My Newest E-Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 715

Yesterday 5313

Week 715

Month 75766

All 6096830

Currently are 222 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments