Exemples de coroutines C++

Exemples De Coroutines C



Les coroutines fournissent une fonctionnalité de langage qui vous permet d'écrire le code asynchrone de manière plus organisée et linéaire, favorisant une approche structurée et séquentielle. Ils fournissent un mécanisme permettant de suspendre et de redémarrer l'exécution d'une fonction à des instances particulières sans arrêter l'ensemble du thread. Les coroutines sont utiles lors de la gestion de tâches nécessitant l'attente d'opérations d'E/S telles que la lecture d'un fichier ou l'envoi d'un appel réseau.

Les coroutines sont basées sur le concept de générateurs où une fonction peut produire des valeurs et être ensuite reprise pour poursuivre l'exécution. Les coroutines fournissent un outil puissant pour gérer les opérations asynchrones et peuvent considérablement améliorer la qualité globale de votre code.

Utilisations des coroutines

Les coroutines sont nécessaires pour plusieurs raisons dans la programmation moderne, en particulier dans des langages comme C++. Voici quelques raisons clés pour lesquelles les coroutines sont bénéfiques :







Les coroutines offrent une solution élégante à la programmation asynchrone. Ils permettent de créer un code d'apparence séquentielle et bloquante, plus simple à raisonner et à comprendre. Les coroutines peuvent suspendre leur exécution à des points spécifiques sans bloquer les threads, permettant ainsi un fonctionnement parallèle d'autres tâches. De ce fait, les ressources du système peuvent être utilisées plus efficacement et la réactivité est accrue dans les applications qui impliquent des opérations d'E/S ou qui attendent des événements externes.



Ils pourraient rendre le code plus facile à comprendre et à maintenir. En éliminant les chaînes de rappel complexes ou les machines à états, les coroutines permettent d'écrire le code dans un style plus linéaire et séquentiel. Cela améliore l'organisation du code, réduit l'imbrication et rend la logique facile à comprendre.



Les coroutines fournissent un moyen structuré de gérer la concurrence et le parallélisme. Ils vous permettent d'exprimer les modèles de coordination complexes et les flux de travail asynchrones à l'aide d'une syntaxe plus intuitive. Contrairement aux modèles de threads traditionnels dans lesquels les threads peuvent être bloqués, les coroutines peuvent libérer les ressources système et permettre un multitâche efficace.





Créons quelques exemples pour démontrer l'implémentation des coroutines en C++.

Exemple 1 : Coroutines de base

L'exemple de coroutines de base est fourni ci-dessous :



#include

#include

structurer CetteCorout {

structurer type_promise {

ThisCorout get_return_object ( ) { retour { } ; }

norme :: suspendre_jamais suspension_initiale ( ) { retour { } ; }

norme :: suspendre_jamais final_suspend ( ) nonsauf { retour { } ; }

vide exception non-gérée ( ) { }

vide retour_void ( ) { }

} ;

bool wait_ready ( ) { retour FAUX ; }

vide attendre_suspendre ( norme :: coroutine_handle <> h ) { }

vide wait_resume ( ) { norme :: cout << 'La Coroutine est reprise.' << norme :: fin ; }

} ;

ThisCorout foo ( ) {

norme :: cout << 'La Coroutine a commencé.' << norme :: fin ;

co_attendre std :: suspendre_always { } ;

co_retour ;

}

int principal ( ) {

auto cr = foo ( ) ;

norme :: cout << 'La Coroutine est créée.' << norme :: fin ;

cr. wait_resume ( ) ;

norme :: cout << 'Coroutine terminé.' << norme :: fin ;

retour 0 ;

}

Passons en revue le code fourni précédemment et expliquons-le en détail :

Après avoir inclus les fichiers d'en-tête requis, nous définissons la structure « ThisCorout » qui représente une coroutine. À l'intérieur de « ThisCorout », une autre structure « promise_type » est définie et gère la promesse de la coroutine. Cette structure fournit diverses fonctions requises par la machinerie coroutine.

Entre parenthèses, nous utilisons la fonction get_return_object(). Il renvoie l'objet coroutine lui-même. Dans ce cas, il renvoie un objet « ThisCorout » vide. Ensuite, la fonction initial_suspend() est invoquée, ce qui détermine le comportement lors du premier démarrage de la coroutine. Le std::suspend_never signifie que la coroutine ne doit pas être suspendue initialement.

Après cela, nous avons la fonction final_suspend() qui détermine le comportement lorsque la coroutine est sur le point de se terminer. Le std::suspend_never signifie que la coroutine ne doit pas être suspendue avant sa finalisation.

Si une coroutine lève une exception, la méthode unhandled_exception() est invoquée. Dans cet exemple, il s'agit d'une fonction vide, mais vous pouvez gérer les exceptions selon vos besoins. Lorsque la coroutine se termine sans donner de valeur, la méthode return_void() est invoquée. Dans ce cas, c’est aussi une fonction vide.

Nous définissons également trois fonctions membres dans « ThisCorout ». La fonction wait_ready() est appelée pour vérifier si la coroutine est prête à reprendre l'exécution. Dans cet exemple, il renvoie toujours false, ce qui indique que la coroutine n'est pas prête à reprendre immédiatement. Lorsque la coroutine va être suspendue, la méthode wait_suspend() est appelée. Ici, c’est une fonction vide ce qui signifie qu’aucune suspension n’est nécessaire. Le programme appelle wait_resume() lorsque la coroutine reprend après une suspension. Il génère simplement un message indiquant que la coroutine a repris.

Les lignes suivantes du code définissent la fonction coroutine foo(). Dans foo(), nous commençons par imprimer un message indiquant que la coroutine a démarré. Ensuite, co_await std::suspend_always{} est utilisé pour suspendre la coroutine et indique qu'elle peut être reprise ultérieurement. L'instruction co_return est utilisée pour terminer la coroutine sans renvoyer aucune valeur.

Dans la fonction main(), nous construisons un objet « cr » de type « ThisCorout » en appelant foo(). Cela crée et démarre la coroutine. Ensuite, un message indiquant que la coroutine a été créée est imprimé. Ensuite, nous appelons wait_resume() sur l'objet coroutine « cr » pour reprendre son exécution. À l'intérieur de wait_resume(), le message « La coroutine est reprise » est imprimé. Enfin, nous affichons un message indiquant que la coroutine est terminée avant la fin du programme.

Lorsque vous exécutez ce programme, le résultat est le suivant :

Exemple 2 : Coroutine avec paramètres et rendement

Maintenant, pour cette illustration, nous fournissons un code qui démontre l'utilisation de coroutines avec des paramètres et du rendement en C++ pour créer un comportement de type générateur afin de produire une séquence de nombres.

#include

#include

#include

structurer NOUVEAUCoroutine {

structurer p_type {

norme :: vecteur < int > valeurs ;

NOUVEAUCoroutine get_return_object ( ) { retour { } ; }

norme :: suspendre_always suspension_initiale ( ) { retour { } ; }

norme :: suspendre_always final_suspend ( ) nonsauf { retour { } ; }

vide exception non-gérée ( ) { }

vide retour_void ( ) { }

norme :: suspendre_always valeur_de rendement ( int valeur ) {

valeurs. repousser ( valeur ) ;

retour { } ;

}

} ;

norme :: vecteur < int > valeurs ;

structurer itérateur {

norme :: coroutine_handle <> chorus_handle ;

opérateur booléen != ( const itérateur & autre ) const { retour chorus_handle != autre. chorus_handle ; }

itérateur & opérateur ++ ( ) { chorus_handle. CV ( ) ; retour * ce ; }

int opérateur * ( ) const { retour chorus_handle. promesse ( ) . valeurs [ 0 ] ; }

} ;

itérateur commencer ( ) { retour itérateur { norme :: coroutine_handle < p_type >:: from_promise ( promesse ( ) ) } ; }

fin de l'itérateur ( ) { retour itérateur { nullptr } ; }

norme :: coroutine_handle < p_type > promesse ( ) { retour
norme :: coroutine_handle < p_type >:: from_promise ( * ce ) ; }

} ;

NOUVEAUCoroutine generateNumbers ( ) {

co_rendement 5 ;

co_rendement 6 ;

co_rendement 7 ;

}

int principal ( ) {

NOUVEAUCoroutine nc = générer des numéros ( ) ;

pour ( int valeur : NC ) {

norme :: cout << valeur << ' ' ;

}

norme :: cout << norme :: fin ;

retour 0 ;

}

Dans le code précédent, la structure NEWCoroutine représente un générateur basé sur une coroutine. Il contient une structure « p_type » imbriquée qui sert de type de promesse pour la coroutine. La structure p_type définit les fonctions requises par la machinerie coroutine telles que get_return_object(), initial_suspend(), final_suspend(), unhandled_exception() et return_void(). La structure p_type inclut également la fonction rendement_value(int value) qui est utilisée pour générer les valeurs de la coroutine. Il ajoute la valeur fournie au vecteur de valeurs.

La structure NEWCoroutine inclut la variable membre std::vector appelée « values » qui représente les valeurs générées. À l’intérieur de NEWCoroutine, il existe un itérateur de structure imbriqué qui permet de parcourir les valeurs générées. Il contient un coro_handle qui est un handle pour la coroutine et définit les opérateurs tels que !=, ++ et * pour l'itération.

Nous utilisons la fonction begin() pour créer un itérateur au début de la coroutine en obtenant le coro_handle de la promesse p_type. Alors que la fonction end() crée un itérateur qui représente la fin de la coroutine et est construit avec un coro_handle nullptr. Après cela, la fonction promise() est utilisée pour renvoyer le type de promesse en créant un coroutine_handle à partir de la promesse p_type. La fonction generateNumbers() est une coroutine qui génère trois valeurs – 5, 6 et 7 – à l'aide du mot-clé co_yield.

Dans la fonction main(), une instance de NEWCoroutine nommée « nc » est créée en appelant la coroutine generateNumbers(). Cela initialise la coroutine et capture son état. Une boucle « for » basée sur une plage est utilisée pour parcourir les valeurs de « nc », et chaque valeur est imprimée et séparée par un espace à l'aide de std :: cout.

Le résultat généré est le suivant :

Conclusion

Cet article démontre l'utilisation des coroutines en C++. Nous avons discuté de deux exemples. Pour la première illustration, la coroutine de base est créée dans un programme C++ à l'aide des fonctions coroutine. Alors que la deuxième démonstration a été réalisée en utilisant les coroutines avec des paramètres et en cédant pour générer un comportement de type générateur pour créer une séquence de nombres.