2023-11-27 12:04:00
Ceci est le troisième article de ma mini-série sur les planificateurs pour les coroutines C++. Les deux premiers articles étaient des articles invités de Dian-Lun Lin :
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++.
Les planificateurs de Dian-Lun étaient basés sur l’adaptateur de conteneur std :: pile et std :: file d’attente. Le std::stack
exécute ses tâches selon la stratégie du dernier entré, premier sorti std::queue
Cependant, selon le principe du premier entré, premier sorti.
L’extrait de code suivant montre le planificateur basé sur une file d’attente :
class Scheduler {
std::queue> _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{};
}
};
Ajouter des priorités à ce planificateur est assez simple.
Un planificateur basé sur une file d’attente prioritaire
std::priority_queue
est a coté de std::stack
et std::queue
le troisième adaptateur de conteneur en C++98.
Mourir std::priority_queue
est le std::queue
similaire. La principale différence est que leur élément le plus volumineux se trouve toujours en haut de la file d’attente prioritaire. std::priority_queue
par défaut, l’opérateur de comparaison est utilisé std::less
. Mourir Nachschlagezeit
dans une std::priority_queue
est constant, mais l’insertion et la suppression sont logarithmiques.
je vais remplacer ça std::queue
dans le planificateur précédent par un std::priority_queue
:
// priority_queueScheduler.cpp
#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;
};
class Scheduler {
// (1)
std::priority_queue>> _prioTasks;
public:
void emplace(int prio, std::coroutine_handle<> task) { // (2)
_prioTasks.push(std::make_pair(prio, task));
}
void schedule() {
while(!_prioTasks.empty()) { // (3)
auto [prio, task] = _prioTasks.top();
_prioTasks.pop();
task.resume();
if(!task.done()) {
_prioTasks.push(std::make_pair(prio, task)); // (4)
}
else {
task.destroy();
}
}
}
auto suspend() {
return std::suspend_always{};
}
};
Task TaskA(Scheduler& sch) {
std::cout << "Hello from TaskAn";
co_await sch.suspend();
std::cout << "Executing the TaskAn";
co_await sch.suspend();
std::cout << "TaskA is finishedn";
}
Task TaskB(Scheduler& sch) {
std::cout << "Hello from TaskBn";
co_await sch.suspend();
std::cout << "Executing the TaskBn";
co_await sch.suspend();
std::cout << "TaskB is finishedn";
}
int main() {
std::cout << 'n';
Scheduler scheduler1;
scheduler1.emplace(0, TaskA(scheduler1).get_handle()); // (5)
scheduler1.emplace(1, TaskB(scheduler1).get_handle());
scheduler1.schedule();
std::cout << 'n';
Scheduler scheduler2;
scheduler2.emplace(1, TaskA(scheduler2).get_handle()); // (6)
scheduler2.emplace(0, TaskB(scheduler2).get_handle());
scheduler2.schedule();
std::cout << 'n';
}
Utilisez d'abord le std::priority_queue
une paire (priorité, poignée) (1). Maintenant, ce couple sera sur le _prioTask
placé (2). Lorsque le planificateur est en cours d'exécution, il vérifie si le _prioTask
est vide (3), sinon la première tâche est appelée, supprimée et reprise. Si la tâche n'est pas terminée, elle sera renvoyée au _prioTasks
reporté (4).
L'utilisation d'un std::priority_queue
a l’effet secondaire agréable que les tâches ayant une priorité plus élevée sont exécutées en premier. L'ordre dans lequel les tâches sont placées sur le planificateur (5 et 6) ne fait aucune différence ; la tâche de priorité 1 s'exécute en premier.
Je voudrais d'abord simplifier la coroutine avant d'améliorer sa gestion des priorités dans mon prochain article.
La coroutine simplifiée
Voici les coroutines précédentes TaskA
et TaskB
:
Task TaskA(Scheduler& sch) {
std::cout << "Hello from TaskAn";
co_await sch.suspend();
std::cout << "Executing the TaskAn";
co_await sch.suspend();
std::cout << "TaskA is finishedn";
}
Task TaskB(Scheduler& sch) {
std::cout << "Hello from TaskBn";
co_await sch.suspend();
std::cout << "Executing the TaskBn";
co_await sch.suspend();
std::cout << "TaskB is finishedn";
}
Au lieu de co_await
dans le planificateur, je le remplace par l'appel direct du waitable prédéfini std::suspend_always
. C'est ainsi que je peux supprimer la fonction de membre de suspension du planificateur. Deuxièmement : la coroutine obtient le nom de sa tâche :
Task createTask(const std::string& name) {
std::cout << name << " startn";
co_await std::suspend_always();
std::cout << name << " executen";
co_await std::suspend_always();
std::cout << name << " finishn";
}
Enfin, voici le programme simplifié avec le résultat correspondant.
// priority_queueSchedulerSimplified.cpp
#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;
};
class Scheduler {
std::priority_queue>> _prioTasks;
public:
void emplace(int prio, std::coroutine_handle<> task) {
_prioTasks.push(std::make_pair(prio, task));
}
void schedule() {
while(!_prioTasks.empty()) {
auto [prio, task] = _prioTasks.top();
_prioTasks.pop();
task.resume();
if(!task.done()) {
_prioTasks.push(std::make_pair(prio, task));
}
else {
task.destroy();
}
}
}
};
Task createTask(const std::string& name) {
std::cout << name << " startn";
co_await std::suspend_always();
std::cout << name << " executen";
co_await std::suspend_always();
std::cout << name << " finishn";
}
int main() {
std::cout << 'n';
Scheduler scheduler1;
scheduler1.emplace(0, createTask("TaskA").get_handle());
scheduler1.emplace(1, createTask(" TaskB").get_handle());
scheduler1.schedule();
std::cout << 'n';
Scheduler scheduler2;
scheduler2.emplace(1, createTask("TaskA").get_handle());
scheduler2.emplace(0, createTask(" TaskB").get_handle());
scheduler2.schedule();
std::cout << 'n';
}
Et après?
Dans mon prochain article, j'améliorerai encore la gestion prioritaire des tâches.
(moi)
#Langage #programmation #planificateur #priorités #pour #les #coroutines
1701382521