Typiquement, les coroutines (et le async/await en général) résolvent un problème que l'on aurait jamais eu si on avait pris le bon design dès le départ.
Le problème initial, c'est de vouloir faire un code avec des callbacks, parce que, asynchrone tout ça... Et de réfléchir à chaque ressource comme si on devait y accéder avec un chemin complet: "s'authentifier, demander X, puis Y, enfin Z".
Du coup, on trouve du bon vieux QNetworkReply avec ses callbacks/signaux Qt imbitables.
En réalité, le fonctionnement de ce type d'API (authentication/authorization/actions) est typiquement une machine à état. Ton client doit prouver un état (il est authentifié, et autorisé à accéder à une ressource) mais après les requêtes sont assez simple en 1:1.
Dans l'exemple précédent, X, Y c'est juste un état. Une fois demandé, le client est X ou Y, tu peux demander Z directement, voire W si W dépends de X uniquement.
Et donc, typiquement, le bon design, sans forcément faire appel aux coroutines, c'est de maintenir une variable d'état et une boucle principale qui agit sur cet état. En pseudo code c'est aussi simple que:
structClient{enumState{NoConnected=0,Authenticated,Authorized,Ready,Requesting,[...]}state;enumResourceType{Me,KVMList,[...]}requiredResource;[...]std::unordered_map<ResourceType,std::function<...>actions;voidsetAction(RessourceTypetype,std::function<...>func){actions[type]=func;}stringanswer;// Where all request answer goes// OVH API helpers herestringme(){setAction(Me,[jsonData]{returnjsonData["me"];});returntriggerAction(Me);}stringtriggerAction(ResourceTypetype){if(state<Ready){// Handle state ramp upwaitMainLoopRampup();}requiredResource=type;state=Requesting;waitMainLoop();state=Ready;returnanswer;}};std::unordered_map<Client::ResourceType,constchar*>ovhAPI={{Client::Me,"/me/"},{Client::KVMList,"/kvmlist/"},[...]};QNetworkReplynam;boolmainLoop(){QUrlurl;switch(client.state){caseNoConnected:url="http://ovhapi/authentification";break;caseAuthenticated:url="http://ovhapi/authorization"+ovhAPI[client.requiredResource];break;caseReady:returntrue;// Nothing to dodefault:returnfalse;}// The only network request elementnam.get(url).connect(client.actions[client.requiredResource]);}
Ici, tout est replié sur un seul niveau d'asynchronisme, et plus des asynchronismes imbriqués. Tu enregistres la fonction que tu veux voir exécutée dans le client, et lorsque tu déclenche l'action d'accéder à la ressource, le client te retourne le résultat, de manière asynchrone, sans que tu n'aies à implémenter toute la logique d'accès à chaque objet qui t'intéresse.
Le mainLoop ci-dessus, c'est en gros le scheduler caché dans les coroutines, sauf que c'est un code qui ne cache rien du coup sans utiliser les trucs comme la sauvegarde de la pile et les pseudo tâches.
Surtout, tu peux exécuter des requêtes en parallèle (tu peux avoir plusieurs threads qui gèrent un mainLoop avec le client partagé), ce qui est très difficile à faire correctement avec les coroutines, car justement lorsqu'une coroutine est en "await", elle est ininterruptible (vu que techniquement, elle n'existe même plus sur la pile d'appel).
# Les coroutines c'est bien, mais c'est pas la panacée
Posté par xryl669 . En réponse au journal Coroutines, histoire d'un nouvel inutilitaire.... Évalué à 5. Dernière modification le 10 novembre 2023 à 10:45.
Typiquement, les coroutines (et le async/await en général) résolvent un problème que l'on aurait jamais eu si on avait pris le bon design dès le départ.
Le problème initial, c'est de vouloir faire un code avec des callbacks, parce que, asynchrone tout ça... Et de réfléchir à chaque ressource comme si on devait y accéder avec un chemin complet: "s'authentifier, demander X, puis Y, enfin Z".
Du coup, on trouve du bon vieux
QNetworkReplyavec ses callbacks/signaux Qt imbitables.En réalité, le fonctionnement de ce type d'API (authentication/authorization/actions) est typiquement une machine à état. Ton client doit prouver un état (il est authentifié, et autorisé à accéder à une ressource) mais après les requêtes sont assez simple en 1:1.
Dans l'exemple précédent, X, Y c'est juste un état. Une fois demandé, le client est X ou Y, tu peux demander Z directement, voire W si W dépends de X uniquement.
Et donc, typiquement, le bon design, sans forcément faire appel aux coroutines, c'est de maintenir une variable d'état et une boucle principale qui agit sur cet état. En pseudo code c'est aussi simple que:
Ici, tout est replié sur un seul niveau d'asynchronisme, et plus des asynchronismes imbriqués. Tu enregistres la fonction que tu veux voir exécutée dans le client, et lorsque tu déclenche l'action d'accéder à la ressource, le client te retourne le résultat, de manière asynchrone, sans que tu n'aies à implémenter toute la logique d'accès à chaque objet qui t'intéresse.
Le
mainLoopci-dessus, c'est en gros le scheduler caché dans les coroutines, sauf que c'est un code qui ne cache rien du coup sans utiliser les trucs comme la sauvegarde de la pile et les pseudo tâches.Surtout, tu peux exécuter des requêtes en parallèle (tu peux avoir plusieurs threads qui gèrent un mainLoop avec le client partagé), ce qui est très difficile à faire correctement avec les coroutines, car justement lorsqu'une coroutine est en "await", elle est ininterruptible (vu que techniquement, elle n'existe même plus sur la pile d'appel).