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.
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.
À 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;
}
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.
Je ne comprends pas l'exemple add-optionals. std::optional n'est pas un objet pouvant être attendu.
@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.
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.
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.
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.
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
Liée : stackoverflow.com/q/35121078/103167
4 votes
Une très bonne et facile à suivre introduction aux coroutines est la présentation de James McNellis "Introduction aux coroutines C++" (Cppcon2016).
2 votes
Enfin, il serait également bon de couvrir "Comment les coroutines en C++ diffèrent-elles des implémentations de coroutines et de fonctions reprises dans d'autres langages ?" (que l'article de Wikipédia lié ci-dessus, étant sans langage spécifique, n'aborde pas).
1 votes
Qui d'autre a lu cette "quarantaine en C++20" ?
2 votes
Lien YouTube vers "Introduction aux coroutines C++" de James McNellis à CppCon 2016 : youtu.be/ZTqHjjm86Bw