Coroutines: A Scheduler for Tasks by Dian-Lun Lin

Coroutines: A Scheduler for Tasks by Dian-Lun Lin

The last post “A Concise Introduction to Coroutines by Dian-Lun Lin” provide the theory. Today, Dian-Lun presents his single-threaded scheduler for C++ coroutines.

This post assumes you are familiar with the previous post “A Concise Introduction to Coroutines by Dian-Lun Lin“.

A Single-threaded Scheduler for C++ Coroutines

In this section, I implement a single-threaded scheduler to schedule coroutines. Let’s begin with the interface:

Task TaskA(Scheduler& sch) {
  std::cout << "Hello from TaskA\n";
  co_await sch.suspend();
  std::cout << "Executing the TaskA\n";
  co_await sch.suspend();
  std::cout << "TaskA is finished\n";
}

Task TaskB(Scheduler& sch) {
  std::cout << "Hello from TaskB\n";
  co_await sch.suspend();
  std::cout << "Executing the TaskB\n";
  co_await sch.suspend();
  std::cout << "TaskB is finished\n";
}


int main() {

  Scheduler sch;

  sch.emplace(TaskA(sch).get_handle());
  sch.emplace(TaskB(sch).get_handle());

  std::cout << "Start scheduling...\n";

  sch.schedule();
        

Both TaskA and TaskB are coroutines. I construct a scheduler in the main function and place the two tasks (coroutine handles) into the scheduler. I then call schedule to schedule the two tasks. A task is a coroutine object that is defined as follows:

struct Task {

  struct promise_type {
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }

    Task get_return_object() { 
        return std::coroutine_handle<promise_type>::from_promise(*this); 
    }
    void return_void() {}
    void unhandled_exception() {}
  };

  Task(std::coroutine_handle<promise_type> handle): handle{handle} {}

  auto get_handle() { return handle; }

  std::coroutine_handle<promise_type> handle;
};
        

Note that I return std::suspend_always in both initial_suspend and final_suspend functions. This is because I want to hand the entire coroutine execution over to the scheduler. Coroutines are executed only after I call schedule. The scheduler is defined as follows:

class Scheduler {

  //std::queue<std::coroutine_handle<>> _tasks;
  std::stack<std::coroutine_handle<>> _tasks;

  public: 

    void emplace(std::coroutine_handle<> task) {
      _tasks.push(task);
    }

    void schedule() {
      while(!_tasks.empty()) {
        //auto task = _tasks.front();
        auto task = _tasks.top();
        _tasks.pop();
        task.resume();

        if(!task.done()) { 
          _tasks.push(task);
        }
        else {
          task.destroy();
        }
      }
    }

    auto suspend() {
      return std::suspend_always{};
    }
};
        

In the scheduler, I store tasks into a stack. I implement emplace method to allow users to push a task into the stack. In schedule method, I keep popping a task from the stack. After resuming a task, I check if that task is done. If not, I push the task back to the stack for later scheduling. Otherwise, I destroy the finished task. After executing the program, the results are the following:

The scheduler stores tasks using a stack (last in, first out). Interestingly, if I replace the stack with the queue (first in, first out), the execution results become:

For completeness, here are both programs:

 



Modernes C++ Mentoring

Be part of my mentoring programs:

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


 

// stackScheduler.cpp

#include <coroutine>
#include <iostream>
#include <stack>


struct Task {

  struct promise_type {
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }

    Task get_return_object() { 
        return std::coroutine_handle<promise_type>::from_promise(*this); 
    }
    void return_void() {}
    void unhandled_exception() {}
  };

  Task(std::coroutine_handle<promise_type> handle): handle{handle} {}

  auto get_handle() { return handle; }

  std::coroutine_handle<promise_type> handle;
};

class Scheduler {

  std::stack<std::coroutine_handle<>> _tasks;

  public: 

    void emplace(std::coroutine_handle<> task) {
      _tasks.push(task);
    }

    void schedule() {
      while(!_tasks.empty()) {
        auto task = _tasks.top();
        _tasks.pop();
        task.resume();

        if(!task.done()) { 
          _tasks.push(task);
        }
        else {
          task.destroy();
        }
      }
    }

    auto suspend() {
      return std::suspend_always{};
    }
};


Task TaskA(Scheduler& sch) {
  std::cout << "Hello from TaskA\n";
  co_await sch.suspend();
  std::cout << "Executing the TaskA\n";
  co_await sch.suspend();
  std::cout << "TaskA is finished\n";
}

Task TaskB(Scheduler& sch) {
  std::cout << "Hello from TaskB\n";
  co_await sch.suspend();
  std::cout << "Executing the TaskB\n";
  co_await sch.suspend();
  std::cout << "TaskB is finished\n";
}


int main() {

  std::cout << '\n';

  Scheduler sch;

  sch.emplace(TaskA(sch).get_handle());
  sch.emplace(TaskB(sch).get_handle());

  std::cout << "Start scheduling...\n";

  sch.schedule();

  std::cout << '\n';

}
        
// queueScheduler.cpp

#include <coroutine>
#include <iostream>
#include <queue>


struct Task {

  struct promise_type {
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }

    Task get_return_object() { 
        return std::coroutine_handle<promise_type>::from_promise(*this); 
    }
    void return_void() {}
    void unhandled_exception() {}
  };

  Task(std::coroutine_handle<promise_type> handle): handle{handle} {}

  auto get_handle() { return handle; }

  std::coroutine_handle<promise_type> handle;
};

class Scheduler {

  std::queue<std::coroutine_handle<>> _tasks;

  public: 

    void emplace(std::coroutine_handle<> task) {
      _tasks.push(task);
    }

    void schedule() {
      while(!_tasks.empty()) {
        auto task = _tasks.front();
        _tasks.pop();
        task.resume();

        if(!task.done()) { 
          _tasks.push(task);
        }
        else {
          task.destroy();
        }
      }
    }

    auto suspend() {
      return std::suspend_always{};
    }
};


Task TaskA(Scheduler& sch) {
  std::cout << "Hello from TaskA\n";
  co_await sch.suspend();
  std::cout << "Executing the TaskA\n";
  co_await sch.suspend();
  std::cout << "TaskA is finished\n";
}

Task TaskB(Scheduler& sch) {
  std::cout << "Hello from TaskB\n";
  co_await sch.suspend();
  std::cout << "Executing the TaskB\n";
  co_await sch.suspend();
  std::cout << "TaskB is finished\n";
}


int main() {

  std::cout << '\n';

  Scheduler sch;

  sch.emplace(TaskA(sch).get_handle());
  sch.emplace(TaskB(sch).get_handle());

  std::cout << "Start scheduling...\n";

  sch.schedule();

  std::cout << '\n';

}
        

What’s Next?

This blog post from Dian-Lun Lin showed a straightforward scheduler for coroutines. I use Dian-Lun’s scheduler in my next post for further experiments.



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, 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, Rob North, Bhavith C Achar, Marco Parri Empoli, moon, and Philipp Lenk.

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, PVS-Studio, Tipi.build, Take Up Code, and SHAVEDYAKS.

Seminars

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

Bookable

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



To view or add a comment, sign in

More articles by Rainer Grimm

  • std::format Extension

    std::format Extension

    Displaying the address of an arbitrary pointer in C++ 20 fails but succeeds with C++26. C++20 Only void, const void…

  • C++26 Library: string and string_view Processing

    C++26 Library: string and string_view Processing

    C++26 offers many small improvements around strings and string_views. First of all: What is a std::string_view A is a…

  • My ALS Journey (18/n): The Never Ending Story

    My ALS Journey (18/n): The Never Ending Story

    Today, I present a sad story, and I can only take it by humor. My ALS Journey so far My Electrical Wheelchair I need…

    3 Comments
  • My Next Mentoring Program: ” Embedded Programming with Modern C++” starts

    My Next Mentoring Program: ” Embedded Programming with Modern C++” starts

    My next mentoring program, “Embedded Programming with Modern C++,” starts on January 31st. Registration is open.

  • Last Chance: 1 Day Left

    Last Chance: 1 Day Left

    December 19, 2024 Make the Difference Let’s do something great together: From December 1st to 24th, when you book one…

  • std::execution: More Senders

    std::execution: More Senders

    offers three types of senders: factories, adapters, and consumers. I’ll take a closer look at these today.

  • Christmas Special – 5 Days Left

    Christmas Special – 5 Days Left

    Make the Difference Let’s do something great together: From December 1st to 24th, when you book one of my mentoring…

  • Christmas Special – 7 Days Left

    Christmas Special – 7 Days Left

    Make the Difference Let’s do something great together: From December 1st to 24th, when you book one of my mentoring…

  • std::execution: Sender

    std::execution: Sender

    std::execution offers three types of senders: factories, adapters, and consumers. I’ll take a closer look at these…

  • My ALS Journey (18/n): C++ and ALS

    My ALS Journey (18/n): C++ and ALS

    Christmas Promotion Let’s do something great together: From December 1st to 24th, when you book one of my mentoring…

Insights from the community

Others also viewed

Explore topics