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 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

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.

 

Rainer D 6 P2 540x540Modernes C++ Mentoring

Be part of my mentoring programs:

 

 

 

 

Do you want to stay informed about my mentoring programs: Subscribe via E-Mail.

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, Animus24, 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, Matthieu Bolt, 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, and Rob North.

 

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

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

My special thanks to PVS-Studio PVC Logo

 

My special thanks to Tipi.build tipi.build logo

 

My special thanks to Take Up Code TakeUpCode 450 60

 

Seminars

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

Bookable (Online)

German

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++

New

  • Clean Code with Modern C++
  • C++20

Contact Me

Modernes C++,

RainerGrimmDunkelBlauSmall

 


Comments   

0 #1 Tarun Ramakrishna El 2021-04-15 18:37
In the pseudo code section after the sentence "Here is the awaiter workflow in prose.", please change awaitable to awaiter.
Quote

Stay Informed about my Mentoring

 

Mentoring

English 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

Course: The All-in-One Guide to C++20

Course: Master Software Design Patterns and Architecture in C++

Subscribe to the newsletter (+ pdf bundle)

All tags

Blog archive

Source Code

Visitors

Today 1000

Yesterday 6503

Week 27257

Month 7503

All 12085712

Currently are 293 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments