2023-12-11 11:25:00
Dans mes deux derniers articles de blog, j’ai introduit un planificateur de priorités pour les coroutines. Le code correspondant contenait une erreur.
Publicité
Rainer Grimm travaille depuis de nombreuses années en tant qu’architecte logiciel, responsable d’équipe et de formation. Il aime écrire des articles sur les langages de programmation C++, Python et Haskell, mais aime également intervenir fréquemment lors de conférences spécialisées. Sur son blog Modern C++, il parle intensément de sa passion C++.
Voici à quoi ressemble le planificateur défectueux :
// 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::from_promise(*this);
}
void return_void() {}
void unhandled_exception() {}
};
Task(std::coroutine_handle handle): handle{handle}{}
auto get_handle() { return handle; }
std::coroutine_handle handle;
};
using job = std::pair>;
template
requires std::invocable && // (2)
std::predicate
class Scheduler {
std::priority_queue, Comperator> _prioTasks;
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 << name << " startn";
co_await std::suspend_always();
for (int i = 0; i <= 3; ++i ) {
std::cout << name << " execute " << i << "n"; // (5)
co_await std::suspend_always();
}
co_await std::suspend_always();
std::cout << name << " finishn";
}
int main() {
std::cout << 'n';
Scheduler scheduler1; // (6)
scheduler1.emplace(0, createTask("TaskA").get_handle());
scheduler1.emplace(1, createTask(" TaskB").get_handle());
scheduler1.emplace(2, createTask(" TaskC").get_handle());
scheduler1.schedule();
std::cout << 'n';
Scheduler 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 << 'n';
}
Voici le résultat du programme que j'ai reçu :
Christof Meerwald a reçu une édition différente auprès du GCC. Merci pour cette astuce. Voici la sortie GCC avec l'optimisation activée.
La sortie Windows était également incorrecte :
Voici les lignes cruciales avec l'erreur :
Task createTask(const std::string& name) { // (1)
std::cout << name << " startn";
co_await std::suspend_always();
for (int i = 0; i <= 3; ++i ) {
std::cout << name << " execute " << i << "n";
co_await std::suspend_always();
}
co_await std::suspend_always();
std::cout << name << " finishn";
}
int main() {
std::cout << 'n';
Scheduler scheduler1;
scheduler1.emplace(0, createTask("TaskA").get_handle()); // (2)
scheduler1.emplace(1, createTask(" TaskB").get_handle()); // (3)
scheduler1.emplace(2, createTask(" TaskC").get_handle());// (4)
scheduler1.schedule();
std::cout << 'n';
Scheduler 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 << 'n';
}
La Coroutine createTask
prend sa chaîne comme référence const lvalue (1), mais ses arguments "TaskA" - "TaskC
" sont des rvalues (2 - 7). L'utilisation d'une référence à une variable temporaire est un comportement indéfini. Les autres planificateurs priority_SchedulerSimplified
et priority_queueSchedulerComparator
dans les articles « Langage de programmation C++ : Un planificateur de priorités pour les coroutines » et «Langage de programmation C++ : un planificateur de priorités sophistiqué pour les coroutines" J'ai le même problème.
Résoudre le problème est facile. Soit prend la coroutine createTask
ihr Argument par valeur an (Task createTask(std::string name)
) ou leurs arguments deviennent des lvalues. Voici la deuxième approche en (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::from_promise(*this);
}
void return_void() {}
void unhandled_exception() {}
};
Task(std::coroutine_handle handle): handle{handle}{}
auto get_handle() { return handle; }
std::coroutine_handle handle;
};
using job = std::pair>;
template
requires std::invocable &&
std::predicate
class Scheduler {
std::priority_queue, Comperator> _prioTasks;
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 << name << " startn";
co_await std::suspend_always();
for (int i = 0; i <= 3; ++i ) {
std::cout << name << " execute " << i << "n";
co_await std::suspend_always();
}
co_await std::suspend_always();
std::cout << name << " finishn";
}
int main() {
std::cout << 'n';
std::string taskA = "TaskA"; // (1)
std::string taskB = " TaskB"; // (2)
std::string taskC = " TaskC"; // (3)
Scheduler scheduler1;
scheduler1.emplace(0, createTask(taskA).get_handle());
scheduler1.emplace(1, createTask(taskB).get_handle());
scheduler1.emplace(2, createTask(taskC).get_handle());
scheduler1.schedule();
std::cout << 'n';
Scheduler 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 << 'n';
}
Et après?
Les coroutines offrent un moyen intuitif d'écrire du code asynchrone. Mon prochain article sera un article invité de Ljubic Damir présentant un workflow mono-producteur - mono-consommateur basé sur des coroutines.
(moi)
#Correction #Bug #dans #planificateur #priorités #pour #les #coroutines #dans #blog
1702325487