Nouvelles Du Monde

Langage de programmation C++ : un planificateur de priorités pour les coroutines

Langage de programmation C++ : un planificateur de priorités pour les coroutines

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.

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.

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';

}



Dans mon prochain article, j'améliorerai encore la gestion prioritaire des tâches.


(moi)

Vers la page d'accueil



#Langage #programmation #planificateur #priorités #pour #les #coroutines
1701382521

Facebook
Twitter
LinkedIn
Pinterest

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

ADVERTISEMENT