Back home

Swift Concurrency Series 01|La raison pour laquelle Swift introduit async/await

Il s'agit de la tentative de Swift de faire passer la concurrence de « selon la convention » à « capacité linguistique »

Lorsque vous verrez async/await pour la première fois, vous le comprendrez comme une « version améliorée de l’écriture de rappel ».

Cette compréhension n’est qu’à moitié correcte.

Si vous souhaitez simplement écrire du code plus court et un style plus synchrone, d’autres langages ont déjà fait des choses similaires. Ce que Swift veut vraiment résoudre, c’est que l’ancien modèle asynchrone est devenu de plus en plus difficile à prendre en charge la complexité des projets iOS modernes.

Dans le passé, il suffisait peut-être de cliquer sur un bouton pour envoyer une demande, mais désormais, une page ordinaire peut impliquer :

  • Chargez plusieurs ressources en parallèle sur le premier écran
  • Annuler les anciennes tâches en quittant la page
  • Le cache local et la requête distante se disputent le même état
  • Actualiser automatiquement le jeton après l’expiration du statut de connexion
  • Les mises à jour de l’interface utilisateur du fil principal et le traitement en arrière-plan sont intercalés

Une fois que l’entreprise atteint cette complexité, le problème asynchrone n’est plus seulement un problème d’écriture, mais un problème de conception de système. L’importance de async/await est précisément qu’il fait passer ce type de problème des « habitudes au niveau de la bibliothèque » aux « règles au niveau du langage ».

1. Le vrai problème avec l’ancien modèle est que le flux de contrôle est interrompu

Tout le monde aime utiliser « l’enfer du rappel » pour expliquer les problèmes de l’ancienne méthode d’écriture asynchrone, mais si vous vous arrêtez uniquement à « l’indentation est trop profonde », vous manquez toujours l’essentiel.

Le vrai problème avec les rappels est que l’activité ** est à l’origine une ligne, mais le code est divisé en plusieurs fragments discrets. **

Par exemple, un processus d’initialisation très courant :

  1. Extrayez les informations sur l’utilisateur actuel
  2. Déterminez le module de la page d’accueil en fonction des autorisations des utilisateurs
  3. Extrayez à nouveau les données de la page d’accueil
  4. En cas d’échec, enregistrez les points cachés
  5. Revenez enfin au fil de discussion principal pour mettre à jour l’interface utilisateur

C’est un lien très clairement séquencé dans mon esprit. Mais à l’ère de l’achèvement, ce lien est souvent divisé en :

  • une clôture de demande
  • Une fermeture de jugement d’autorisation
  • Plusieurs couches de gestion des erreurs
  • Un interrupteur de fil principal
  • Plusieurs succursales à retour anticipé

À mesure que le code s’allonge, il devient difficile de l’expliquer d’un seul coup d’œil :

  • Quel est le processus principal ?
  • Quelles branches seront cassées
  • Qui doit être blâmé lorsqu’une étape échoue ?
  • Faut-il continuer après avoir quitté la page ?

La vraie difficulté du maintien d’une logique asynchrone est que l’intention et l’expression sont déconnectées.

2. Le plus gros problème avec la complétion est qu’elle laisse trop de sémantiques clés à la convention

l’achèvement n’est pas une mauvaise chose. De nombreuses API sous-jacentes sont encore très utiles aujourd’hui et conviennent toujours à certains scénarios nécessitant plusieurs rappels, une sortie en continu et une connexion avec des systèmes plus anciens.

Le problème est que lorsqu’un système repose fortement sur l’achèvement, une grande partie de la sémantique importante n’est que des « habitudes d’équipe » plutôt que des règles linguistiques.

Par exemple, les éléments suivants sont souvent compris par défaut dans le modèle d’achèvement :

  • Ce rappel sera-t-il appelé une ou plusieurs fois ?
  • Le succès et l’échec s’excluent-ils strictement ?
  • Dans quel fil le rappel reviendra-t-il ?
  • Si l’appelant a la possibilité d’annuler
  • Si l’objet est libéré à mi-chemin, les résultats doivent-ils continuer à être délivrés ?

Vous constaterez que ce sont les problèmes fondamentaux des systèmes concurrents.

Une fois que ces sémantiques sont maintenues par la documentation, la dénomination et l’expérience, plus le projet est vaste, plus il est facile pour les sémantiques de diverger. En fin de compte, la chose la plus pénible est généralement l’incapacité de raisonner de manière stable sur la manière dont un morceau de code asynchrone s’exécutera.

3. Swift introduit async/await, qui renforce essentiellement les règles du code asynchrone.

La valeur de async/await ne se limite pas à :

fetchUser { result in
  ...
}

Remplacer par :

let user = try await fetchUser()

Plus important encore, cela ramène au niveau linguistique de nombreuses choses qui étaient initialement dispersées dans la convention.

Par exemple maintenant :

  • Une fonction async a un point de retour clair
  • L’échec est propagé via throw plutôt que par un style différent de rappel de résultat
  • await indique explicitement qu’il s’agit du point de pause
  • La chaîne d’appels asynchrones peut être lue sans sauter entre plusieurs fermetures.

L’importance de cette affaire n’est pas « l’apparence est belle », mais « les règles sont plus strictes ».

La véritable crainte des systèmes complexes réside toujours dans les règles floues. Une fois les règles assouplies, le code devient de plus en plus dépendant de « cette personne comprend simplement le contexte ».

4. Cela améliore non seulement la lisibilité, mais aussi le caractère raisonnable

Une situation courante est la suivante : je peux comprendre l’achèvement, pourquoi dois-je apprendre async/await ?

La question n’est pas de savoir si vous pouvez le comprendre aujourd’hui, mais :

  • Pouvez-vous encore comprendre rapidement après trois mois ?
  • Lorsque des collègues prennent le relais, peuvent-ils déduire le processus à partir du code lui-même ?
  • Lorsqu’un bug survient, est-il possible de marcher de l’entrée à la sortie ?

C’est la différence entre « lisibilité » et « caractère raisonnable ».

La lisibilité est « Ce code est-il agréable à mes yeux aujourd’hui ? » Le caractère raisonnable est “Puis-je juger sur la base de ce code : comment l’échec est transmis, comment l’annulation prend effet et à quel niveau le statut est modifié.”

Par exemple, les problèmes suivants sont souvent très difficiles à résoudre dans l’ancien modèle :

  • Une fois que l’utilisateur quitte la page, cette chaîne de requêtes existe-t-elle toujours ?
  • Lorsque la troisième étape échoue, où s’arrêtera le statut final de la page ?
  • Lorsque deux résultats asynchrones reviennent en même temps, qui est qualifié pour rédiger l’UI ?

async/await ne répondra certainement pas automatiquement à ces questions pour l’équipe, mais il permettra au moins de placer ces questions dans une structure de processus plus claire plutôt que de les cacher dans des fermetures fragmentées.

5. Cette question devient de plus en plus critique dans les projets iOS

L’un des changements les plus importants apportés aujourd’hui au projet iOS est que « l’asynchronie est passée d’une capacité marginale à une capacité de base ».

Dans le passé, l’asynchrone consistait davantage à « cliquer et envoyer une demande ». Participe désormais directement de manière asynchrone au cycle de vie des pages, à la machine à états, à la couche de cache, au système d’autorisations, au système de points cachés et participe même à la conception de la vitesse de réponse de l’ensemble du produit.

Une situation courante pour une vraie page d’entreprise est :

  • Il y aura une première demande d’écran dès l’ouverture de la page.
  • Certains modules utilisent le cache local pour échapper
  • Certaines requêtes reposent sur des profils utilisateurs ou des configurations expérimentales
  • Certaines opérations nécessitent une prévention de la réentrée
  • Certains résultats doivent être renvoyés au thread principal pour changer d’état en toute sécurité

Selon ce principe, si le système asynchrone consiste encore simplement à « chacun écrit sa propre complétion », le code passera bientôt de « peut fonctionner » à « personne n’ose changer ».

Ce que async/await comprend vraiment, c’est que l’asynchrone est devenu la méthode de travail par défaut dans les applications modernes.

6. C’est l’entrée de tout le modèle de concurrence de Swift.

Si vous regardez uniquement cette fonctionnalité grammaticale, async/await semble être simplement une meilleure expression asynchrone.

Mais si vous regardez Swift Concurrency dans son ensemble, cela ouvre la voie à des fonctionnalités ultérieures :

  • Task : clarifier les limites des tâches et le cycle de vie
  • MainActor : Contraindre la sémantique d’exécution des états liés à l’interface utilisateur
  • Actor : Isoler l’état mutable partagé
  • Concurrence structurée : incorporer des sous-tâches dans le cycle de vie de la tâche parent

En d’autres termes, Swift n’essaie pas seulement de fournir une API asynchrone plus pratique, il essaie de transformer la « concurrence » d’une technique fragmentaire en un ensemble de modèles composables, raisonnables et vérifiables par le langage.

Cela montre également que async/await ne peut pas être considéré comme simplement « plus facile à écrire ». Ce qui est réellement lié à cela, c’est un tout nouvel ensemble de philosophies de conception de concurrence.

7. Cela ne résout pas automatiquement tous les problèmes, mais cela change la nature du problème.

Il faut aussi le préciser ici : avec async/await, les bugs de concurrence ne disparaîtront pas automatiquement.

Cela ne résout pas :

  • Mauvaise conception des frontières de l’État
  • Les anciennes tâches écrasent les nouveaux résultats
  • Un ViewModel assume trop de responsabilités en même temps
  • La stratégie d’annulation de mission n’est pas du tout réfléchie

Mais cela change une chose très importante : De nombreux problèmes ne sont plus « parce que l’expression est trop confuse et que je ne vois même pas à quoi ressemble le problème », mais « le problème a été clairement exprimé ».

Cela peut ressembler à un pas en arrière, mais il s’agit en réalité d’un pas de géant en avant. Car ce n’est que lorsque le problème devient exprimable que le débogage, la révision et la refactorisation peuvent être efficaces.

8. Conclusion : Swift introduit async/wait pour ne pas écrire moins de fermetures

Pour le dire de manière plus courte, je dirais :

Swift introduit async/await pour rendre à nouveau le code asynchrone compréhensible, raisonnable et maintenable.

C’est donc la première étape vers la mise à niveau du modèle de concurrence.

Une fois que vous aurez compris cela, lorsque vous examinerez Task, Actor et MainActor plus tard, vous ne les considérerez pas comme des points de syntaxe dispersés, mais vous comprendrez que ce que fait Swift est une chose plus importante : **Reclassez la concurrence de « compétences empiriques » à « règles linguistiques ». **

FAQ

What to read next

Related

Continue reading