Back home

Swift Concurrency Series 03|Comment comprendre Task, Task.tached et MainActor

Ils apparaissent souvent ensemble, mais ils répondent respectivement à trois questions : « D'où vient la tâche, qui y est lié et qui modifie l'interface utilisateur ?

Lorsque je suis entré en contact avec Swift Concurrency pour la première fois, j’étais confus à propos de Task, Task.detached et MainActor :

  • Tout ce qui concerne l’asynchrone
  • apparaissent souvent dans le même morceau de code
  • Tout cela ressemble à “laisser le code s’exécuter quelque part”

La façon la plus courante d’apprendre est donc de mémoriser les définitions. Cependant, si vous vous basez uniquement sur les définitions pour mémoriser ces trois concepts, il est facile de se perdre en les apprenant. Parce qu’ils ressemblent à des concepts similaires, ils répondent en réalité à trois types de questions complètement différents.

Une façon plus pratique de le comprendre est la suivante :

  • Task : Comment démarrer la mission
  • Task.detached : dans quelle mesure la tâche est liée au contexte actuel
  • MainActor : Dans quelle sémantique d’isolement cette logique doit-elle être exécutée ?

Brisez simplement ces trois dimensions et une grande partie de la confusion disparaîtra immédiatement.

1. Task résout : Comment puis-je entrer dans le processus asynchrone à partir d’ici ?

Les scénarios d’utilisation les plus naturels pour Task sont :

Le code actuel n’est pas async, mais je dois maintenant entrer dans un processus asynchrone.

C’est très courant sous iOS :

  • Le rappel par clic sur le bouton n’est pas async
  • La méthode proxy UIKit n’est pas async
  • Un certain événement du cycle de vie synchrone déclenche soudainement une requête asynchrone

Par exemple :

Button("刷新") {
  Task {
    await viewModel.reload()
  }
}

La signification de Task ici est très claire : relier un portail d’interaction synchrone au monde asynchrone.

Ainsi, la clé de Task est « la limite de tâche est créée ». Une fois écrit, cela signifie :

  • Ici commence un travail asynchrone avec un cycle de vie indépendant
  • il peut être annulé
  • It will end at some point
  • Ses conséquences doivent finalement être gérées par quelqu’un

Cela montre également que Task ne peut pas être simplement compris comme « une couche peut être utilisée pour await ».

2. Les caractéristiques réelles du Task ordinaire : Il poursuit généralement une période de travail asynchrone dans le contexte actuel

Une situation courante est de comprendre le Task ordinaire de manière trop « indépendante », en pensant que dès sa création, il n’a rien à voir avec le contexte actuel.

Une compréhension plus précise de l’ingénierie devrait être :

**Le Task commun est souvent une tâche qui s’étend du contexte actuel. **

Cela signifie qu’il hérite souvent d’une partie de l’environnement actuel, comme :

  • Sémantique actuelle des acteurs
  • Contexte actuel de la tâche
  • Certaines annulations de relations
  • Tendances prioritaires actuelles

Cela ressemble donc plus à “entrer en asynchrone au-dessus de la logique actuelle” plutôt qu’à “créer un nouvel univers complètement décalé”.

Il est important de comprendre cela, car vous comprendrez plus tard de quoi exactement le Task.detached se « désengage ».

3. Task.detached est une déclaration indépendante plus forte

De nombreux articles décrivent le Task.detached comme une version « plus avancée », ce qui peut facilement conduire à des biais dans la pratique.

C’est plus dangereux.

La raison est simple : fondamentalement, il est moins probable qu’elle hérite du contexte actuel.

Autrement dit, en écrivant :

Task.detached {
  ...
}

En fait, il exprime :

  • Ce travail ne veut pas être naturellement lié au contexte actuel
  • Je veux que ce soit plus indépendant
  • Je suis prêt à assumer moi-même davantage de responsabilités en matière de cycle de vie et d’isolement

Ceci est raisonnable dans quelques scénarios, tels que :

  • Effectuer un travail de nettoyage en arrière-plan qui n’a pas grand-chose à voir avec la page actuelle
  • Réaliser certaines tâches qui nécessitent évidemment de rompre avec la sémantique actuelle des acteurs
  • Certains frameworks ou couches d’infrastructure nécessitent explicitement des tâches indépendantes

Mais dans le secteur des pages, ce que la plupart des gens veulent réellement, c’est quelque chose de plus gérable. Et detached rend souvent la gestion difficile.

4. Task.detached est facilement utilisé à mauvais escient

Parce que le premier sentiment que cela procure aux gens est celui de « liberté ».

Mais dans les systèmes concurrents, le prix de la liberté est souvent le débordement de responsabilités.

Une fois que vous aurez utilisé le Task.detached, vous devrez bientôt à nouveau répondre à ces questions :

  • À qui appartient-il maintenant ?
  • Doit-il continuer lorsque la page est détruite ?
  • Si la tâche externe est annulée, sera-t-elle toujours exécutée ?
  • Peut-il changer directement l’état actuel après son retour ?

Si aucune de ces questions ne trouve une réponse claire, Task.detached aboutit généralement à une évasion de la responsabilité de la mission.

Mon propre principe par défaut est donc simple :

  • Utilisez d’abord le Task ordinaire pour les couches de page et ViewModel.
  • Ne considérez Task.detached que si vous savez très clairement « pourquoi il faut sortir du contexte actuel »

5. MainActor n’est pas du tout le concept de « démarrage d’une tâche ».

C’est le point qui mérite d’être clarifié le plus en profondeur.

Task et Task.detached discutent :

Comment une tâche asynchrone est créée et dans quelle mesure elle est étroitement liée au contexte actuel.

MainActor aborde :

Sous quelle sémantique d’isolement un certain morceau de code doit être exécuté.

Il ne s’agit pas d’une « version du thread principal de la tâche », ni d’une « tâche spécifiquement utilisée pour mettre à jour l’interface utilisateur ». It essentially tells the compiler and caller:

  • Cette logique appartient au domaine de l’isolement des acteurs principaux
  • C’est fortement lié à l’interface utilisateur
  • Ne peut être modifié de manière aléatoire dans aucun contexte concurrent

Par conséquent, le MainActor a toujours été axé sur les « contraintes ».

6. Les codes liés à l’interface utilisateur doivent être pris au sérieux MainActor

Une situation courante consiste à penser : de toute façon, en fin de compte, nous attribuons simplement une valeur à la page, donc cela ne devrait pas être si grave.

Le problème est qu’un état d’interface utilisateur vraiment complexe ne se voit jamais attribuer une seule valeur.

Les vraies pages font souvent coexister ces éléments :

  • état de chargement
  • Liste des données
  • État vide
  • invite d’erreur
  • Certains boutons locaux sont désactivés

Une fois que ces valeurs sont écrites par plusieurs résultats asynchrones à différents moments, sans limites claires de MainActor, le problème s’accumulera lentement dans :

  • La page clignote de temps en temps
  • Some status updates are in weird order
  • ViewModel fait la navette entre la logique d’arrière-plan et la logique de l’interface utilisateur

Par conséquent, la valeur de MainActor n’est pas seulement de « prévenir les erreurs de thread », mais également d’établir une limite de propriété claire pour les états de l’interface utilisateur.

7. Une séquence de jugement plus proche du combat réel

Si vous ne savez pas quel concept utiliser lors de l’écriture du code, vous pouvez d’abord vous poser la question dans l’ordre suivant :

1. Suis-je dans un contexte non asynchrone et dois-je entrer dans un processus asynchrone ?

Si tel est le cas, considérez d’abord Task.

2. Ai-je vraiment besoin que cette tâche existe indépendamment du contexte actuel ?

Considérez Task.detached uniquement si la réponse est très claire. S’il « semble simplement plus libre », il ne devrait généralement pas être utilisé.

3. Ce code lit-il et écrit-il un statut fortement lié à l’interface utilisateur ?

Si tel est le cas, vous devriez sérieusement envisager le MainActor au lieu d’attendre que quelque chose se passe mal.

Cette séquence de jugement est bien plus utile que la mémorisation des définitions d’API car elle correspond directement aux problèmes réels rencontrés lors de l’écriture du code métier.

8. À quoi ressemble la mauvaise odeur la plus courante ?

Les trois types de mauvaises odeurs les plus courants que je vois sont :

1. Partout où le await n’est pas disponible, emballez d’abord un Task.

Cela rendra les entrées de tâches dans le projet de plus en plus fragmentées et, au final, personne ne pourra dire à qui appartiennent ces tâches.

2. Je ne comprends pas l’héritage de contexte et utilisez toujours Task.detached

Cela semble très « indépendant », mais en fait, cela ne fait souvent que pousser plus loin le problème du cycle de vie.

3. ViewModel est responsable à la fois du traitement en arrière-plan et de la réécriture de l’interface utilisateur, mais il n’y a pas de limite claire pour MainActor.

Ce type de code ne plante pas nécessairement à court terme, mais il est particulièrement sujet à accumuler des erreurs d’état cachées à long terme.

9. Conclusion : Ils ne sont pas dans la même dimension

Si je devais retenir ces trois concepts en une seule phrase, je dirais ceci :

  • Task : Je souhaite maintenant créer une tâche asynchrone
  • Task.detached : Je souhaite créer une tâche asynchrone plus indépendante et moins héritée du contexte actuel.
  • MainActor : Cette logique doit être exécutée dans la limite d’isolement de l’acteur principal

Ils répondent à différentes questions dans les systèmes concurrents :

  • Où commence la mission ?
  • Dans quelle mesure la tâche est liée au contexte actuel
  • Quels Etats doivent être isolés par l’acteur principal

Tant que ces trois dimensions sont séparées, lors de l’écriture ultérieure du code Swift Concurrency, « démarrer une tâche » et « revenir au thread principal » ne seront plus confondus avec la même chose.

FAQ

What to read next

Related

Continue reading