Que sont les coroutines en C ++ 20?


104

Que sont les coroutines dans ?

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

L'image ci-dessous provient de l'ISOCPP.

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

entrez la description de l'image ici


3
Répondre "En quoi le concept de coroutines est-il différent du parallélisme et de la concurrence ?" - en.wikipedia.org/wiki/Coroutine
Ben Voigt


3
Une très bonne introduction à la coroutine facile à suivre est la présentation de James McNellis «Introduction aux coroutines C ++» (Cppcon2016).
philsumuru

2
Enfin, il serait également bon de couvrir "En quoi les coroutines en C ++ sont-elles différentes des implémentations de coroutines et de fonctions pouvant être reprises dans d'autres langages?" (dont l'article de wikipedia lié ci-dessus, étant indépendant de la langue, ne traite pas)
Ben Voigt

1
qui d'autre a lu cette "quarantaine en C ++ 20"?
Sahib Yar

Réponses:


201

À un niveau abstrait, Coroutines a séparé 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 "threads d'exécution" mais un seul état d'exécution (il ne fonctionne que sur plusieurs données). On peut dire que les algorithmes parallèles sont un peu comme ça, en ce sens que vous avez un «programme» exécuté sur des données différentes.

Le thread a 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 un état, mais il n'a pas de thread d'exécution.


L'exemple le plus simple de coroutines sont les générateurs ou énumérables d'autres langages.

En pseudo code:

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

Le Generatorest appelé, et la première fois qu'il est appelé, il revient 0. Son état est mémorisé (son état varie avec l'implémentation des coroutines), et la prochaine fois que vous l'appelez, il continue là où il s'était arrêté. Donc, il renvoie 1 la prochaine fois. Puis 2.

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

Les coroutines apportent cette capacité au C ++.

Il existe deux types de coroutines; empilable et sans pile.

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

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

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

Le processus de production d'une valeur est appelé «rendement», car les coroutines sont un peu comme le multithreading coopératif; vous cédez 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 empilées sont plus puissantes, mais aussi plus chères.


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 if, les boucles et les appels de fonctions, sont un autre type de "goto structuré" qui vous permet d'exprimer certains modèles utiles (comme les machines à états) d'une manière plus naturelle.


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

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

Une fonction devient une coroutine en ayant l'une de celles-ci dans son corps. Donc, d'après leur déclaration, ils sont indiscernables des fonctions.

Lorsqu'un de ces trois mots-clés est utilisé dans un corps de fonction, un examen obligatoire standard du type de retour et des arguments se produit et la fonction est transformée en coroutine. Cet examen 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<int> get_integers( int start=0, int step=1 ) {
  for (int current=start; true; current+= step)
    co_yield current;
}

co_yieldsuspend l'exécution des fonctions, stocke cet état dans le generator<int>, puis renvoie la valeur de à currenttravers le generator<int>.

Vous pouvez boucler sur les entiers renvoyés.

co_awaitpendant ce temps, vous permet d'épisser 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 y êtes co_await. S'ils sont prêts, vous procédez immédiatement; sinon, vous suspendez jusqu'à ce que l'attendable que vous attendez soit prêt.

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_dataest une coroutine qui génère une std::futurelorsque la ressource nommée est ouverte et que nous parvenons à analyser jusqu'au point où nous avons trouvé les données demandées.

open_resourceet read_lines sont probablement des coroutines asynchrones qui ouvrent un fichier et en lisent des lignes. Le co_awaitconnecte l'état de suspension 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 en plus des types d'espace utilisateur. Les types d'espace utilisateur définissent efficacement ce que co_return co_awaitet co_yield signifient - j'ai vu des gens l'utiliser pour implémenter des expressions optionnelles monadiques de sorte qu'un co_awaitsur un optionnel vide propage automatiquement l'état vide vers le optionnel 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;
}

26
C'est l'une des explications les plus claires de ce que sont les coroutines que j'ai jamais lues. Les comparer et les distinguer des fils SIMD et classiques était une excellente idée.
Omnifarious

2
Je ne comprends pas l'exemple des add-optionnels. std :: optional <int> n'est pas un objet attendu.
Jive Dadson

1
@mord oui il est censé renvoyer 1 élément. Pourrait avoir besoin de polissage; si nous voulons plus d'une ligne, nous avons besoin d'un flux de contrôle différent.
Yakk - Adam Nevraumont

1
@lf désolé, c'était censé l'être ;;.
Yakk - Adam Nevraumont

1
@LF pour une fonction aussi simple, il n'y a peut-être pas de différence. Mais la différence que je vois en général est qu'une coroutine se souvient du point d'entrée / sortie (exécution) dans son corps alors qu'une fonction statique démarre l'exécution depuis le début à chaque fois. L'emplacement des données «locales» est sans importance, je suppose.
avp

21

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

Une implémentation de coroutine expérimentale précédente de Microsoft utilisait des piles copiées afin que vous puissiez même 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 Boosts.


1

les coroutines sont censées être des fonctions (en C ++) capables d '«attendre» qu'une autre routine se termine et de fournir tout ce qui est nécessaire pour que la routine suspendue, suspendue, en attente continue. la fonctionnalité la plus intéressante pour les gens de C ++ est que les coroutines ne prendraient idéalement pas d'espace dans la pile ... C # peut déjà faire quelque chose comme ça avec await et yield, mais C ++ devra peut-être être reconstruit pour l'inclure.

la concurrence est fortement axée sur la séparation des préoccupations lorsqu'une préoccupation est une tâche que le programme est censé accomplir. cette séparation des préoccupations peut être accomplie par un certain nombre de moyens ... généralement une délégation de quelque sorte. l'idée de concurrence est qu'un certain nombre de processus pourraient s'exécuter indépendamment (séparation des préoccupations) et qu'un «auditeur» dirigerait tout ce qui est produit par ces préoccupations séparées vers l'endroit où il est censé aller. cela dépend fortement d'une sorte de gestion asynchrone. Il existe un certain nombre d'approches de la concurrence, y compris la programmation orientée Aspect et d'autres. C # a l'opérateur «délégué» qui fonctionne très bien.

le parallélisme ressemble à la concurrence et peut être impliqué, mais il s'agit en fait 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 portions de code vers différents processeurs où il sera exécuté et les résultats seront reçus en retour de manière synchrone.


9
La concurrence et la séparation des préoccupations sont totalement indépendantes. Les coroutines ne doivent pas fournir d'informations pour la routine suspendue, ce sont les routines pouvant être reprises.
Ben Voigt
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.