Que sont les coroutines dans c ++ 20?
En quoi il est différent de "Parallelism2" ou / et "Concurrency2" (regardez l'image ci-dessous)?
L'image ci-dessous provient de l'ISOCPP.
Que sont les coroutines dans c ++ 20?
En quoi il est différent de "Parallelism2" ou / et "Concurrency2" (regardez l'image ci-dessous)?
L'image ci-dessous provient de l'ISOCPP.
Réponses:
À 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 Generator
est 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_yield
ainsi 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_yield
suspend l'exécution des fonctions, stocke cet état dans le generator<int>
, puis renvoie la valeur de à current
travers le generator<int>
.
Vous pouvez boucler sur les entiers renvoyés.
co_await
pendant 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_data
est une coroutine qui génère une 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_line
s sont probablement des coroutines asynchrones qui ouvrent un fichier et en lisent des lignes. Le co_await
connecte 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_await
et co_yield
signifient - j'ai vu des gens l'utiliser pour implémenter des expressions optionnelles monadiques de sorte qu'un co_await
sur 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;
}
;;
.
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.
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.