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:
// 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, 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,and Matt Godbolt.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
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)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,