139 votes

Qu'est-ce que sont les coroutines en C++20 ?

Qu'est-ce que sont les coroutines en c++20?

En quoi est-ce différent de "Parallelism2" et/ou "Concurrency2" (voir l'image ci-dessous)?

L'image ci-dessous provient de ISOCPP.

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

entrez la description de l'image ici

3 votes

Pour répondre à la question "en quoi le concept de coroutines est-il différent de parallélisme et de concurrency?" -- fr.wikipedia.org/wiki/Coroutine

0 votes

4 votes

Une très bonne et facile à suivre introduction aux coroutines est la présentation de James McNellis "Introduction aux coroutines C++" (Cppcon2016).

270voto

Yakk Points 31636

À un niveau abstrait, les Coroutines divisent l'idée d'avoir un état d'exécution de l'idée d'avoir un fil d'exécution.

SIMD (single instruction multiple data) a plusieurs "fils d'exécution" mais un seul état d'exécution (il fonctionne simplement sur plusieurs données). De manière discutable, les algorithmes parallèles sont un peu comme ça, dans le sens où vous avez un "programme" qui s'exécute sur des données différentes.

Le threading a plusieurs "fils d'exécution" et plusieurs états d'exécution. Vous avez plus d'un programme et plus d'un fil d'exécution.

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


L'exemple le plus simple des 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
}

Le Generator est appelé, et la première fois qu'il est appelé, il retourne 0. Son état est mémorisé (la quantité d'état variant selon l'implémentation des coroutines), et la prochaine fois que vous l'appelez, il continue là où il s'est arrêté. Ainsi, il retourne 1 la fois suivante. Puis 2.

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

Les Coroutines apportent cette fonctionnalité à C++.

Il existe deux types de coroutines ; avec pile (stackful) et sans pile (stackless).

Une coroutine sans pile stocke uniquement les variables locales dans son état et sa position d'exécution.

Une coroutine avec pile stocke une pile entière (comme un fil d'exécution).

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 dans quelque chose un peu similaire à un lambda ; toutes les variables locales vont dans l'état d'un objet, et des étiquettes sont utilisées pour sauter vers/depuis l'emplacement où la coroutine "produit" des résultats intermédiaires.

Le processus de production d'une valeur est appelé "yield", car les coroutines sont un peu comme un multitraitement coopératif ; vous cédez le point d'exécution à l'appelant.

Boost propose une implémentation de coroutines avec pile ; cela vous permet d'appeler une fonction pour suspendre pour vous. Les coroutines avec pile sont plus puissantes, mais aussi plus coûteuses.


Il y a plus dans les coroutines qu'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 Coroutines, comme les instructions conditionnelles, les boucles et les appels de fonctions, sont une autre sorte de "goto structuré" qui vous permet d'exprimer certaines structures utiles (comme des machines à états) de manière plus naturelle.


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

À son niveau le plus élémentaire, elle ajoute quelques mots-clés à C++ : co_return co_await co_yield, ainsi que quelques types de bibliothèque qui fonctionnent avec eux.

Une fonction devient une coroutine en ayant l'un de ces mots-clés dans son corps. Ainsi, à partir de leur déclaration, elles sont indiscernables des fonctions.

Lorsque l'un de ces trois mots-clés est utilisé dans un corps de fonction, une vérification standard mandatée du type de retour et des arguments a lieu et la fonction est transformée en une coroutine. Cette vérification indique au compilateur où stocker l'état de la fonction lorsque la fonction est suspendue.

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

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

co_yield suspend l'exécution de la fonction, stocke cet état dans le generator, puis retourne la valeur de current à travers le generator.

Vous pouvez boucler sur les entiers retournés.

co_await permet quant à lui d'insérer une coroutine dans une autre. Si vous êtes dans une coroutine et que vous avez besoin des résultats d'une chose pouvant être attendue (souvent une coroutine) avant de progresser, vous faites un co_await dessus. S'ils sont prêts, vous continuez immédiatement; sinon, vous suspendez jusqu'à ce que la chose attendue sur laquelle vous attendez soit prête.

std::future> load_data( std::string resource )
{
  auto handle = co_await open_resouce(resource);
  while( auto line = co_await read_line(handle)) {
    if (std::optional 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 à parser jusqu'au point où nous avons trouvé les données demandées.

open_resource et read_line sont probablement des coroutines asynchrones qui ouvrent un fichier et lisent des lignes. Le co_await connecte l'état de suspension et prêt de load_data à leur progression.

Les coroutines C++ sont bien plus flexibles que cela, car elles ont été implémentées comme un ensemble minimal de fonctionnalités de langage sur des types en espace utilisateur. Les types en espace utilisateur définissent efficacement ce que co_return co_await et co_yield signifient - j'ai vu des gens les utiliser pour implémenter des expressions optionnelles monadiques de telle sorte qu'un co_await sur un optionnel vide propage automatiquement l'état vide à l'extérieur de l'optionnel :

modified_optional add( modified_optional a, modified_optional b ) {
  co_return (co_await a) + (co_await b);
}

au lieu de

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

38 votes

Ceci est l'une des explications les plus claires de ce que sont les coroutines que j'ai jamais lues. Comparer et les distinguer des SIMD et des threads classiques était une excellente idée.

3 votes

Je ne comprends pas l'exemple add-optionals. std::optional n'est pas un objet pouvant être attendu.

0 votes

@jive Je pense que le code actuel comportait une option augmentée pour être attendue. L'effet était que si on attendait sur une option non-"prête", elle se suspendrait et renverrait une option vide, et serait prête si l'option était renseignée. Cela se compose bien et vous permet d'exprimer "si cette option est vide arrêtez le calcul et renvoyez vide, sinon continuez". C'était un peu une astuce, et cela aurait peut-être nécessité une allocation de tas.

23voto

Lothar Points 4740

Une coroutine est comme une fonction C qui a plusieurs instructions de retour et lorsque appelée une 2ème fois, elle ne commence pas l'exécution au début de la fonction mais à la première instruction après le retour précédemment exécuté. Cet emplacement d'exécution est enregistré avec toutes les variables automatiques qui vivraient sur la pile dans les fonctions non coroutine.

Une implémentation précédente expérimentale de coroutine chez Microsoft utilisait des piles copiées afin de pouvoir même retourner de fonctions profondément imbriquées. 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 Boost.

3 votes

Pourquoi est-ce que c'est "comme une fonction C" plutôt que "comme une fonction"?

0voto

Dr t Points 244

Les coroutines sont censées être des fonctions (en C++) capables de "attendre" qu'une autre routine se termine et de fournir tout ce dont la routine suspendue, en pause, en attente, a besoin pour continuer. La caractéristique la plus intéressante pour les adeptes de C++ est que les coroutines idéalement ne prendraient aucun espace de pile... C# peut déjà faire quelque chose de similaire avec await et yield, mais C++ pourrait devoir être reconstruit pour l'obtenir.

La concurrence est fortement axée sur la séparation des préoccupations où une préoccupation est une tâche que le programme est censé accomplir. Cette séparation des préoccupations peut être réalisée par divers moyens... généralement par délégation de quelque sorte. L'idée de la concurrence est qu'un certain nombre de processus pourraient s'exécuter de manière indépendante (séparation des préoccupations) et un 'auditeur' dirigerait tout ce qui est produit par ces préoccupations séparées vers sa destination prévue. Cela dépend fortement d'une sorte de gestion asynchrone. Il existe plusieurs approches de la concurrence, y compris la programmation orientée aspect et d'autres. C# a l'opérateur 'delegué' qui fonctionne très bien.

Le parallélisme ressemble à la concurrence et peut être impliqué, mais il s'agit en réalité d'une construction physique impliquant de nombreux processeurs disposé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 reçus de manière synchrone.

12 votes

La concurrence et la séparation des préoccupations sont totalement sans rapport. Les coroutines ne sont pas là pour fournir des informations à la routine suspendue, elles sont les routines réutilisables.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X