In my last two blog posts I introduced a priority scheduler for coroutines. The code for this had an error.
Advertisement
Rainer Grimm has been working as a software architect, team and training manager for many years. He enjoys writing articles on the programming languages āāC++, Python and Haskell, but also enjoys speaking frequently at specialist conferences. On his blog Modern C++ he deals intensively with his passion C++.
This is what the faulty scheduler looks like:
// priority_queueSchedulerPriority.cpp
#include
#include
#include
#include
#include
#include
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
}
void return_void() {}
void unhandled_exception() {}
};
Task(std::coroutine_handle
auto get_handle() { return handle; }
std::coroutine_handle
};
using job = std::pair
template
requires std::invocable
std::predicate
class Scheduler {
std::priority_queue
public:
void emplace(int prio, std::coroutine_handle task) {
_prioTasks.push(std::make_pair(prio, task));
}
void schedule() {
Updater upd = {}; // (3)
while(!_prioTasks.empty()) {
auto [prio, task] = _prioTasks.top();
_prioTasks.pop();
task.resume();
if(!task.done()) {
_prioTasks.push(std::make_pair(upd(prio), task)); // (4)
}
else {
task.destroy();
}
}
}
};
Task createTask(const std::string& name) {
std::cout scheduler2; // (7)
scheduler2.emplace(0, createTask(āTaskAā).get_handle());
scheduler2.emplace(1, createTask(ā TaskBā).get_handle());
scheduler2.emplace(2, createTask(ā TaskCā).get_handle());
scheduler2.schedule();
std::cout This was the output of the program that I received:
Christof Meerwald received a different edition with the GCC. Thank you for this hint. Here is the GCC output with optimization enabled.
The Windows output was also incorrect:
Here are the crucial lines with the error:
Task createTask(const std::string& name) { // (1)
std::cout scheduler2;
scheduler2.emplace(0, createTask(āTaskAā).get_handle()); // (5)
scheduler2.emplace(1, createTask(ā TaskBā).get_handle()); // (6)
scheduler2.emplace(2, createTask(ā TaskCā).get_handle());// (7)
scheduler2.schedule();
std::cout The createTask coroutine takes its string as a const lvalue reference (1), but its arguments āTaskAā ā āTaskCā are rvalues āā(2 ā 7). It is undefined behavior to use a reference to a temporary variable. The other schedulers priority_SchedulerSimplified and priority_queueSchedulerComparator in the articles āC++ programming language: A priority scheduler for coroutinesā and āC++ programming language: A sophisticated priority scheduler for coroutinesā have the same problem.
Fixing the problem is easy. Either the createTask coroutine takes its argument by Value (Task createTask(std::string name)) or its arguments become lvalues. Here is the second approach in (1) ā (3):
// priority_queueSchedulerPriority.cpp
#include
#include
#include
#include
#include
#include
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
}
void return_void() {}
void unhandled_exception() {}
};
Task(std::coroutine_handle
auto get_handle() { return handle; }
std::coroutine_handle
};
using job = std::pair
template
requires std::invocable
std::predicate
class Scheduler {
std::priority_queue
public:
void emplace(int prio, std::coroutine_handle task) {
_prioTasks.push(std::make_pair(prio, task));
}
void schedule() {
Updater upd = {};
while(!_prioTasks.empty()) {
auto [prio, task] = _prioTasks.top();
_prioTasks.pop();
task.resume();
if(!task.done()) {
_prioTasks.push(std::make_pair(upd(prio), task));
}
else {
task.destroy();
}
}
}
};
Task createTask(const std::string& name) {
std::cout scheduler2;
scheduler2.emplace(0, createTask(taskA).get_handle());
scheduler2.emplace(1, createTask(taskB).get_handle());
scheduler2.emplace(2, createTask(taskC).get_handle());
scheduler2.schedule();
std::cout Whatās next?
Coroutines provide an intuitive way to write asynchronous code. My next post will be a guest post by Ljubic Damir presenting a single-producer ā single-consumer workflow based on coroutines. (rme)
To home page