web-dev-qa-db-fra.com

Que sont les coroutines en C ++ 20?

Que sont les coroutines dans c ++ 2 ?

En quoi cela diffère-t-il de "Parallelism2" ou/et "Concurrency2" (regardez dans l'image ci-dessous)?

L'image ci-dessous provient de ISOCPP.

https://isocpp.org/files/img/wg21-timeline-2017-03.png

enter image description here

77
Pavan Chandaka

À un niveau abstrait, Coroutines a divisé l’idée d’avoir un état d’exécution de l’idée d’avoir un fil d’exécution.

SIMD (une seule instruction, plusieurs données) a plusieurs "threads d'exécution" mais un seul état d'exécution (il ne fonctionne que sur plusieurs données). Les algorithmes parallèles sont un peu comme ceci, en ce sens que vous avez un "programme" exécuté sur des données différentes.

Les threads comportent plusieurs "threads d'exécution" et plusieurs états d'exécution. Vous avez plus d'un programme et plus d'un thread d'exécution.

Coroutines a plusieurs états d'exécution, mais ne possède pas de thread d'exécution. Vous avez un programme, et le programme a l'état, mais il n'a pas de thread d'exécution.


Les exemples les plus simples de coroutines sont les générateurs ou les énumérables d'autres langages.

En pseudo code:

function Generator() {
  for (i = 0 to 100)
    produce i
}

La Generator est appelée et la première fois, elle renvoie 0. Son état est mémorisé (son état varie en fonction de la mise en œuvre des coroutines) et la prochaine fois que vous l'appelez, il continue là où il s'est arrêté. Donc, il retourne 1 la prochaine fois. Puis 2.

Finalement, il atteint la fin de la boucle et tombe en fin de fonction; la coroutine est terminée. (Ce qui se passe ici varie en fonction du langage dont nous parlons; en python, cela jette une exception).

Les coroutines apportent cette capacité au C++.

Il existe deux types de coroutines; empilés et sans pile.

Une coroutine sans pile ne stocke que les variables locales dans son état et son lieu d'exécution.

Une coroutine empilée stocke une pile entière (comme un fil).

Les coroutines sans pile peuvent être extrêmement légères. La dernière proposition que j'ai lue impliquait essentiellement de réécrire votre fonction en quelque chose qui ressemblerait un peu à un lambda; toutes les variables locales entrent dans l'état d'un objet, et les étiquettes sont utilisées pour aller à/de l'endroit où la coroutine "produit" des résultats intermédiaires.

Le processus de production d'une valeur s'appelle "rendement", car les routines sont un peu comme le multithreading coopératif; vous rendez le point d’exécution à l’appelant.

Boost a une implémentation de coroutines empilées; il vous permet d’appeler une fonction à céder pour vous. Les coroutines empilables sont plus puissantes, mais aussi plus chères.


Les coroutines ne se résument pas à un simple générateur. Vous pouvez attendre une coroutine dans une coroutine, ce qui vous permet de composer des coroutines de manière utile.

Les routines, comme si, les boucles et les appels de fonction, sont un autre type de "goto structuré" qui vous permet d'exprimer certains modèles utiles (comme les machines à états) de manière plus naturelle.


L'implémentation spécifique de Coroutines en C++ est un peu intéressante.

Au niveau le plus élémentaire, il ajoute quelques mots-clés au C++: co_returnco_awaitco_yield, ainsi que des types de bibliothèque qui fonctionnent avec eux.

Une fonction devient une coroutine en ayant une de celles-ci dans son corps. Donc, de leur déclaration, ils sont indissociables des fonctions.

Lorsqu'un de ces trois mots-clés est utilisé dans un corps de fonction, un examen standard obligatoire du type de retour et des arguments est effectué et la fonction est transformée en une coroutine. Cet examen indique au compilateur où stocker l'état de la fonction lorsque celle-ci est suspendue.

La coroutine la plus simple est un générateur:

generator<int> get_integers( int start=0, int step=1 ) {
  for (int current=start; true; current+= step)
    co_yield current;
}

co_yield suspend l'exécution des fonctions, enregistre cet état dans le generator<int>, puis renvoie la valeur de current par le biais du generator<int>.

Vous pouvez faire une boucle sur les entiers retournés.

co_await pendant ce temps, vous permet de coller une coroutine sur une autre. Si vous êtes dans une coroutine et que vous avez besoin des résultats d'une chose attendue (souvent une coroutine) avant de progresser, vous co_await dessus. S'ils sont prêts, vous procédez immédiatement; sinon, vous suspendez la séance jusqu'à ce que l'attente que vous attendez soit prête.

std::future<std::expected<std::string>> load_data( std::string resource )
{
  auto handle = co_await open_resouce(resource);
  while( auto line = co_await read_line(handle)) {
    if (std::optional<std::string> r = parse_data_from_line( line ))
       co_return *r;
  }
  co_return std::unexpected( resource_lacks_data(resource) );
}

load_data est une coroutine qui génère un std::future lorsque la ressource nommée est ouverte et que nous parvenons à analyser jusqu'au point où nous avons trouvé les données demandées.

open_resource et read_lines sont probablement des coroutines asynchrones qui ouvrent un fichier et en lisent les lignes. Le co_await connecte les états suspendu et prêt de load_data à leur progression.

Les coroutines C++ sont beaucoup plus flexibles que cela, car elles ont été implémentées comme un ensemble minimal de fonctionnalités de langage par-dessus les types d'espace utilisateur. Les types d'espace utilisateur définissent efficacement ce que co_returnco_await et co_yieldmoyenne - J'ai vu des gens l'utiliser pour implémenter des expressions optionnelles monadiques telles qu'un co_await sur une option optionnelle vide propage automatiquement l'état vide à l'option optionnelle externe:

modified_optional<int> add( modified_optional<int> a, modified_optional<int> b ) {
  return (co_await a) + (co_await b);
}

au lieu de

std::optional<int> add( std::optional<int> a, std::optional<int> b ) {
  if (!a) return std::nullopt;
  if (!b) return std::nullopt;
  return *a + *b;
}
148

Une coroutine est semblable à une fonction C qui a plusieurs instructions de retour et qui, une seconde fois, ne commence pas l'exécution au début de la fonction mais à la première instruction après le retour précédent. Cet emplacement d'exécution est enregistré avec toutes les variables automatiques qui vivraient sur la pile dans des fonctions non coroutines.

Une précédente implémentation de coroutine expérimentale de Microsoft utilisait des piles copiées pour vous permettre même de revenir à partir de fonctions imbriquées profondes. Mais cette version a été rejetée par le comité C++. Vous pouvez obtenir cette implémentation par exemple avec la bibliothèque de fibres optiques Boosts.

11
Lothar

les coroutines sont supposées être (en C++) des fonctions capables d '"attendre" qu'une autre routine soit terminée et de fournir tout le nécessaire pour la routine suspendue, en pause, en attente ou continue. La caractéristique la plus intéressante pour les gens de C++ est que les coroutines ne prendraient idéalement pas d'espace de pile ... C # peut déjà faire quelque chose comme ceci avec wait et renvoyer, mais il faudra peut-être reconstruire C++ pour le récupérer.

la simultanéité est fortement axée sur la séparation des préoccupations lorsqu'une préoccupation est une tâche que le programme est censé mener à bien. cette séparation des préoccupations peut être réalisée par un certain nombre de moyens ... généralement une délégation. L'idée de la simultanéité est qu'un certain nombre de processus peuvent fonctionner indépendamment (séparation des problèmes) et qu'un "auditeur" dirige tout ce qui est produit par ces problèmes séparés vers l'endroit où il est supposé aller. cela dépend fortement d'une sorte de gestion asynchrone. Il existe un certain nombre d'approches de la simultanéité, notamment la programmation orientée aspect. C # a l'opérateur 'délégué' qui fonctionne plutôt bien.

le parallélisme sonne comme une concurrence et peut être impliqué mais est en réalité une construction physique impliquant de nombreux processeurs agencés de manière plus ou moins parallèle avec un logiciel capable de diriger des parties de code vers différents processeurs où il sera exécuté et les résultats seront renvoyés. de manière synchrone.

1
Dr t