Et c’est ainsi que c’est arrivé. Après une longue période de doutes, d’opposition et de préparation de cette fonctionnalité, WG21 s’est mis d’accord sur l’apparence des coroutines, et les coroutines entreront probablement en C 20. Comme il s’agit d’une fonctionnalité importante, je pense qu’il est bon de commencer à la préparer et à l’apprendre dès maintenant (rappelez-vous qu’il existe également des modules, des concepts et des gammes dans la liste à apprendre).
Beaucoup de personnes s’opposaient à cette fonctionnalité. Les principales plaintes concernaient la dureté à comprendre, de nombreux points de personnalisation et peut-être des performances non optimales en raison d’allocations de mémoire dynamique potentiellement non optimisées (peut-être 😉).
A lire en complément : Choisir les accessoires parfaits selon votre morphologie : le guide ultime
Il y a même eu des tentatives parallèles au TS accepté (spécifications techniques officiellement publiées) pour créer un autre mécanisme de coroutines. Coroutines, nous allons discuter ici sont celles décrites dans le TS (spécification technique), et c’est le document à fusionner avec l’IS (norme internationale). L’alternative, en revanche, était créé par Google. Après tout, l’approche de Google s’est avérée également souffrir de nombreux problèmes, qui n’étaient pas anodins à résoudre, nécessitant souvent d’étranges fonctionnalités supplémentaires en C .
La conclusion finale a été d’accepter les coroutines de Microsoft (les auteurs du TS). Et c’est ce dont nous allons parler dans cet article. Commençons donc par…
A voir aussi : Les chaussures tendance : quelles formes choisir pour parfaire votre tenue ?
Plan de l'article
Quels sont les coroutines ?
Les coroutines existent déjà dans de nombreux langages de programmation, que ce soit Python ou C#. Les coroutines fournissent un autre moyen de créer du code asynchrone. En quoi cela diffère des threads et pourquoi avons-nous besoin d’une fonctionnalité linguistique dédiée aux coroutines et enfin comment nous pouvons en bénéficier seront expliqués dans cette section.
Il y a beaucoup de malentendus concernant ce qu’est la coroutine. Selon l’environnement dans lequel ils sont utilisés, ils peuvent être appelés :
- coroutines
- empilables coroutines
- fils verts
- fibres
- goroutines
empilables
La bonne nouvelle est que les coroutines empilées, les fils verts, les fibres, les goroutines sont la même chose (parfois utilisés différemment). Nous les appellerons plus tard les fibres ou les coroutines empilées. Mais il y a quelque chose de spécial à propos des coroutines empilables, qui sont le sujet principal de cette série d’articles (vous pouvez vous attendre bientôt à d’autres articles sur les coroutines et leur utilisation).
Pour comprendre les coroutines et avoir une certaine intuition à leur sujet, nous allons d’abord examiner brièvement les fonctions et (ce que nous pourrions appeler) « leur API ». La méthode standard de leur utilisation consiste simplement à les appeler et à attendre qu’ils aient fini :
Après avoir appelé la fonction, il n’y a aucun moyen de la suspendre ou de la reprendre. Les seules opérations sur les fonctions que nous pouvons effectuer sont le début et la fin void foo () { return ; //ici nous quittons la fonction } foo () ; //ici on appelle/démarre la fonction . Une fois la fonction démarrée, nous devons attendre qu’elle soit fini. Si nous appelons à nouveau la fonction, elle commence son exécution depuis le début.
La situation avec les coroutines est différente. Vous pouvez non seulement le démarrer et l’arrêter, mais aussi le suspendre et le reprendre. Il est toujours différent du fil du noyau car les coroutines ne sont pas préemptives en elles-mêmes (les coroutines en revanche appartiennent généralement au fil, qui est préemptif). Pour le comprendre, regardons le générateur défini dans le python. Même si le monde Python appelle ce générateur, il serait appelé coroutine dans le langage C . L’exemple est tiré de ce site Web :
def generate_nums () : num = 0 tandis que True : Num de rendement num = num 1 nums = generate_nums () pour x en chiffres : imprimé (x) si x >
Le fonctionnement de ce code est que l’appel à la fonction generate_nums crée un objet coroutine. Chaque fois que nous parcourons l’objet coroutine, la coroutine est reprise et se suspend une fois que le mot-clé yield est rencontré, retournant le prochain entier dans la séquence (la boucle for est un sucre syntaxique pour le prochain appel de fonction, qui reprend la coroutine). Le code termine la boucle lorsqu’il rencontre l’instruction break. Dans ce cas, la coroutine ne finit jamais, mais il est facile d’imaginer la situation dans laquelle la coroutine atteint sa fin et se termine. Nous voyons maintenant que ce type de coroutine peut être démarré, suspendu, repris et enfin terminé 9 : casser
. Les Coroutines comme bibliothèque.
Vous avez donc maintenant l’intuition de ce que sont les coroutines. Vous savez qu’il existe déjà des bibliothèques permettant de créer des objets en fibres. La question est alors de savoir pourquoi avons-nous besoin d’une fonctionnalité linguistique dédiée et pas seulement d’une bibliothèque, qui permette l’utilisation des coroutines.
Cela tente de répondre à cette question et de vous montrer la différence entre coroutines empilables et empilables. La différence est la clé pour comprendre la caractéristique du langage coroutine.
Coroutines empilables
Parlons d’abord de ce que sont des coroutines empilées, de leur fonctionnement et des raisons pour lesquelles elles peuvent être implémentées en tant que bibliothèque. Ils peuvent être plus faciles à expliquer car ils sont construits de la même manière que les threads.
Les fibres ou les coroutines empilables sont une pile séparée qui peut être utilisée pour traiter les appels de fonction. Pour comprendre exactement comment fonctionne ce type de coroutine, nous allons examiner brièvement les cadres de fonction et les appels de fonction du point de vue de bas niveau. Mais d’abord, regardons les propriétés des fibres.
- ils ont leur propre pile,
- la durée de vie des fibres est indépendante du code qui l’a appelée (généralement elles peuvent avoir un planificateur défini par l’utilisateur),
- fibres peuvent être détachées d’un thread et attachées à un autre,
- planification coopérative (la fibre doit décider de passer à un autre fibre/scheduler),
- ne peut pas fonctionner simultanément sur le même thread.
les
Les implications pour les propriétés mentionnées sont les suivantes :
- le changement de contexte de la fibre doit être effectué par l’utilisateur des fibres, et non par le système d’exploitation (le système d’exploitation peut toujours disposer de la fibre en dépossédant le thread sur lequel elle s’exécute),
- aucune véritable course de données ne se produit entre deux fibres fonctionnant sur le même thread puisqu’une seule peut être active,
- développeur de fibre doit savoir quand c’est un endroit et un moment appropriés pour redonner de la puissance de calcul au planificateur ou à l’appelé possible.
- opérations d’E/S dans la fibre doivent être asynchrones afin que les autres fibres puissent faire leur travail sans se bloquer mutuellement.
le
Les
Expliquons maintenant le fonctionnement des fibres dans le détail à partir de l’explication de ce que fait la pile pour les appels de fonction.
La pile est donc un bloc contigu de la mémoire, qui est nécessaire pour stocker les variables locales et les arguments de la fonction. Mais ce qui est encore plus important, après chaque appel de fonction (à quelques exceptions près), des informations supplémentaires sont placées sur la pile pour savoir à la fonction appelée comment retourner à l’appelé et restaurer les registres du processeur.
Certains registres ont un but particulier et sont enregistrés sur la pile lors d’appels de fonction. Ces registres (dans le cas de l’architecture ARM) sont les suivants :
- SP — pointeur de pile
- LR — registre de lien
- PC — compteur de programme
Un pointeur de pile est un registre qui contient l’adresse du début de la pile, qui appartient à l’appel de fonction en cours. Grâce à cette valeur, il est facile de se référer aux arguments et aux variables locales, qui sont enregistrés dans la pile.
Le registre des liens est très important lors des appels de fonction. Il stocke l’adresse de retour (adresse de l’appelé) où il y a un code à exécuter une fois l’exécution de la fonction en cours terminée. Quand le est appelée le PC est enregistré dans le LR. Lorsque la fonction est renvoyée, le PC est restauré à l’aide de la LR.
Le compteur de programme est l’adresse de l’instruction en cours d’exécution.
Chaque fois qu’une fonction est appelée, le registre des liens est enregistré, afin que cette fonction sache où retourner une fois terminée.
Comportement des registres PC et LR lors de l’appel et du retour de fonction
Lorsque la coroutine empilable est exécutée, les fonctions appelées utilisent la pile précédemment allouée pour stocker ses arguments et ses variables locales. Étant donné que les appels de fonction stockent toutes les informations de la pile pour la coroutine empilable, la fibre peut suspendre son exécution dans n’importe quelle fonction appelée dans la coroutine.
Voyons maintenant ce qui se passe dans l’image ci-dessus. Tout d’abord, les fils et les fibres ont leurs propres piles séparées. Les chiffres verts sont le numéro de commande dans lequel les actions se produisent
- La fonction régulière appel à l’intérieur du fil. Effectue une allocation de pile.
- La fonction crée l’objet en fibre. En conséquence, la pile pour la fibre est allouée. La création de la fibre ne signifie pas nécessairement qu’elle est exécutée immédiatement. En outre, le cadre d’activation est alloué. Les données de la trame d’activation sont définies de telle sorte que l’enregistrement de leur contenu dans les registres du processeur entraîne le changement de contexte vers la pile de fibres.
- Appel de fonction régulier.
- Appel Coroutine. Les registres du processeur sont réglés sur le contenu de la trame d’activation.
- Appel de fonction régulier à l’intérieur de la coroutine.
- Appel de fonction régulier à l’intérieur de la coroutine.
- Coroutine se suspend. Le contenu de la trame d’activation est mis à jour et les registres du processeur sont définis, de sorte que le contexte retourne dans la pile du thread.
- Appel de fonction régulier à l’intérieur du thread.
- Appel de fonction régulier à l’intérieur du thread.
- Reprise du coroutine — Une chose similaire se produit pendant l’appel de coroutine. Le cadre d’activation se souvient de l’état des registres du processeur à l’intérieur de la coroutine, qui ont été réglés pendant la suspension de la coroutine.
- Appel de fonction régulier à l’intérieur de la coroutine. Cadre de fonction alloué dans la pile de la coroutine.
- Une certaine simplification de l’image est faite. Ce qui se passe maintenant, c’est que la coroutine se termine et que la pile est débobinée. Mais le retour de la coroutine se produit en fait par la fonction inférieure (et non supérieure).
- Retour de fonction normal comme ci-dessus.
- Retour de fonction normal.
- retour de Coroutine. La pile de la coroutine est vide. Le contexte est rebasculé vers le fil de discussion. À partir de maintenant, la fibre ne peut plus être rétablie.
- Un appel de fonction normal dans le contexte du thread.
- Plus tard, les fonctions peuvent continuer l’opération ou se terminer, de sorte que la pile soit finalement débobinée.
Le
Dans le cas des coroutines empilées, il n’est pas nécessaire de une fonction linguistique dédiée pour les utiliser. Des coroutines empilées entières peuvent simplement être implémentées à l’aide de la bibliothèque et il existe déjà des bibliothèques conçues pour cela :
- https://swtch.com/libtask/
- https://code.google.com/archive/p/libconcurrency/
- https://www.boost.org Boost.Fiber
- https://www.boost.org Boost.Coroutine
Parmi ceux mentionnés, seul Boost est la bibliothèque C , car les autres ne sont que des bibliothèques C.
Les détails du fonctionnement des bibliothèques peuvent être consultés dans la documentation. Mais en gros, toutes ces bibliothèques seront capables de créer la pile séparée pour la fibre et offriront la possibilité de reprendre (à partir de l’appelant) et de suspendre (de l’intérieur) la coroutine.
Jetons un coup d’œil à l’exemple de Boost.Fiber :
#include #include #include #include #include #include #include en ligne void fn (std : :string const& str, int n) { pour (int i = 0 ; i < n ; i) { std : :cout