WebFront C++23
EN ↗GitHub ↗

Tome II / CHAPITRE 04

Pour comprendre et contribuer

Quand une opération n’est pas terminée

Fermer la fenêtre ne fait pas disparaître une lecture en attente. C’est là que commencent les questions de concurrence.

Conventions de notation

Les noms de fonctions, types, commandes, options et fichiers sont composés en chasse fixe, par exemple describe, wf test ou main.cpp. L’italique introduit un terme lors de sa définition. Les blocs de code donnent les extraits à saisir ; le texte qui les précède précise leur emplacement et leurs prérequis.

Pendant que nous attendons

Un navigateur connecté peut rester silencieux pendant longtemps. Consacrer un thread bloqué à chacune de ces attentes serait coûteux. Les boucles HTTP et WebSocket utilisent donc des coroutines, qui suspendent leur exécution lorsqu’elles attendent une opération du fournisseur réseau.

La suspension ne crée pas un nouveau thread. Si nous lançons un long calcul dans un callback du pont, ce calcul occupe toujours le contexte serveur. Les autres opérations qui en dépendent attendront leur tour. De même, une attente bloquante sur ce contexte peut empêcher la réponse qu’elle attend de progresser.

Avant de déplacer du travail ou de partager une donnée, il est utile de retrouver le contexte qui exécutera chaque morceau. Les tâches démarrées par WebFront reprennent sur le contexte serveur ; le frontend suit les contraintes de son toolkit.

Garder la trace du travail commencé

Il ne suffit pas de savoir qu’une opération existe ; il faut aussi savoir qui attendra sa fin. Le serveur réunit ses opérations réseau et ses tâches applicatives dans un networking::TaskScope, fondé sur un counting_scope.

Une tâche applicative renvoie webfront::Task. Après spawn, l’application la possède. Le TaskHandle permet de lui demander de s’arrêter, mais sa destruction ne supprime pas la tâche. Sans cette distinction, jeter un handle pourrait faire disparaître du travail dont d’autres objets attendent encore le résultat.

Si nous utilisons directement WebSocket ou WebLink, nous devons fournir une portée extérieure. Elle doit vivre assez longtemps pour que les tâches aient terminé leurs derniers accès aux connexions et à leurs dépendances.

Fermer sans libérer trop tôt

Imaginons maintenant que la fenêtre se ferme alors qu’une lecture est en attente. Nous voulons que cette lecture se termine, puis que les objets qu’elle utilisait soient libérés. Les détruire dans l’ordre inverse laisserait l’opération reprendre avec des références pendantes.

stop() demande l’arrêt. shutdown() demande l’arrêt et attend la fin du travail possédé. Ce dernier appel se fait hors des callbacks serveur, après le retour des appelants externes de run() ou de runOne().

Une demande peut provenir d’un autre thread ; elle ne lui donne pas pour autant le droit de fermer les objets réseau. Ceux-ci terminent leurs opérations sur leur propre contexte. Le drainage conserve les propriétaires et fait avancer ce contexte jusqu’à la jointure.

La réponse arrive trop tard

Un appel au navigateur peut encore être en cours lorsque sa tâche C++ est arrêtée. Le sender termine alors l’attente localement. Le JavaScript déjà lancé continue ; une réponse tardive sera écartée. Les futures et les promesses ordinaires n’exposent pas cette annulation.

Il peut aussi arriver que le résultat et la demande d’arrêt surviennent presque ensemble. Nous devons obtenir une seule terminaison, quel que soit leur ordre. Un test qui impose ces deux ordres nous apprend davantage qu’un test qui espère rencontrer la course par hasard.

Enfin, demander l’arrêt suppose que l’opération puisse coopérer. Une opération qui ne termine jamais peut bloquer la destruction. L’arrêt explicite signale une jointure sans progression possible ; le destructeur conserve les propriétaires et attend.

Vérifier sa compréhension

Une demande d’arrêt suffit-elle à autoriser la destruction des dépendances ?