Swift Concurrency Series 06|Problèmes courants dans la concurrence Swift : conditions de concurrence, requêtes répétées et confusion d'état
Le véritable problème est que ces problèmes se manifestent souvent par des pannes sporadiques dans l’entreprise plutôt que par des pannes explicites.
La chose la plus frustrante à propos des bugs de concurrence est qu’ils ne ressemblent souvent pas à des bugs.
Cela se manifeste le plus souvent en ligne sous la forme de ces questions ambiguës :
- L’utilisateur a dit “parfois ça clignote”
- Le test indique “Des données parfois anciennes apparaissent”
- Le produit disait “Je viens de couper le filtre, pourquoi a-t-il rebondi à nouveau ?”
- Il n’y a pas de crash évident dans le journal, mais l’état de la page est tout simplement erroné.
En d’autres termes, de nombreux problèmes de concurrence ressemblent davantage à des « exceptions commerciales occasionnelles » qu’à des problèmes « manifestement techniquement défectueux ».
Ainsi, dans cet article, je ne veux pas seulement parler de la définition des termes, mais me concentrer directement sur un scénario de page de liste plus réel et décomposer les trois types de problèmes les plus courants :
- Concurrence
- Répéter la demande
- état de confusion
Et comment ils grandissent dans le vrai code.
1. Regardez d’abord une page qui est si réelle qu’elle ne pourrait pas être plus réelle.
Supposons qu’il existe une page de liste d’articles qui prend en charge ces opérations :
- Chargement automatique lorsque la page entre pour la première fois
- Déroulez vers le bas pour actualiser
- Changer de catégorie
- Entrez la recherche par mot-clé
- Cliquez sur “Réessayer”
De nombreux projets sont écrits ainsi au début :
@MainActor
final class ArticlesViewModel: ObservableObject {
@Published var items: [Article] = []
@Published var isLoading = false
@Published var errorMessage: String?
@Published var selectedCategory: String = "all"
@Published var keyword: String = ""
let repository: ArticlesRepository
init(repository: ArticlesRepository) {
self.repository = repository
}
func onAppear() {
Task {
await load()
}
}
func refresh() {
Task {
await load()
}
}
func retry() {
Task {
await load()
}
}
func categoryChanged(to value: String) {
selectedCategory = value
Task {
await load()
}
}
func keywordChanged(to value: String) {
keyword = value
Task {
await load()
}
}
func load() async {
isLoading = true
errorMessage = nil
do {
items = try await repository.fetchArticles(
category: selectedCategory,
keyword: keyword
)
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
}
Lorsque ce code est écrit pour la première fois, tout le monde pense généralement qu’il est « assez fluide » :
- Oui
async/await - Le code est simple
- Chaque entrée fonctionne
Mais tant que la page est réellement utilisée, des problèmes de concurrence surgiront bientôt.
2. Premier type de problème : la condition de concurrence critique est un ordre par défaut qui n’existe pas.
Toujours ce code.
Son principal problème n’est pas qu’il ouvre beaucoup de Task, mais que ces choses se produisent par défaut dans l’ordre souhaité :
- La demande envoyée en premier sera renvoyée en premier.
- Lorsque l’ancienne requête revient, les conditions de filtrage actuelles n’ont pas changé.
- Le début et la fin du chargement correspondent toujours à un à un
Mais les systèmes asynchrones ne garantissent pas ces commandes pour l’équipe.
Par exemple, l’utilisateur opère comme suit :
- Entrez dans la page et demandez à A d’émettre
- Passez immédiatement à la catégorie “iOS” et demandez à B d’envoyer
- Saisissez à nouveau le mot-clé
swiftpour demander à C d’émettre
À ce moment, si l’ordre de retour est :
- C revient en premier
- Revenez après A
- B revient en dernier
Selon le code actuel, les trois résultats seront remplacés par items.
En d’autres termes, ce qui est affiché sur la page finale dépend de qui revient en dernier, et non de qui correspond à l’intention actuelle de l’utilisateur.
Il s’agit de la condition de course la plus typique :
Le code s’appuie secrètement sur l’ordre, mais l’ordre n’est pas du tout contraint.
3. Le deuxième type de problème : La cause première des demandes répétées est généralement que l’entrée n’est pas fermée.
En regardant le ViewModel ci-dessus, il y a au moins cinq entrées qui déclencheront load() :
onAppearrefreshretrycategoryChangedkeywordChanged
Chaque entrée a son propre Task.
C’est certainement légal d’un point de vue syntaxique, mais d’un point de vue technique, cela signifie :
- Il n’existe pas de point de planification unifié pour des tâches similaires
- Personne ne sait si une tâche similaire est déjà en cours
- Lorsque de nouvelles tâches apparaissent, les anciennes tâches n’ont pas de sort clair
Les « demandes répétées » ne sont alors plus accidentelles, mais un produit naturel de la structure.
Ainsi, en gestion de la concurrence, je demande rarement :
“Pourquoi y a-t-il une demande supplémentaire ici ?”
Je demande plus souvent :
“Combien d’entrées existe-t-il pour le même type de tâches ? Existe-t-il des relations de substitution entre elles ?”
Si vous ne pouvez pas répondre à ces deux questions, des demandes répétées sont presque inévitables.
4. Troisième type de problème : Le statut est désordonné, souvent parce que les résultats expirés peuvent encore être rédigés.
Une situation courante est que tant que la demande est renvoyée avec succès, le résultat doit être accepté.
Cela convient généralement aux systèmes synchrones, mais est souvent erroné dans les systèmes concurrents.
Parce que le problème le plus critique dans un scénario simultané est :
**Ce résultat est-il toujours considéré comme un résultat valide pour la page actuelle ? **
Par exemple :
- La page actuelle est passée à
keyword = "swift" - Le résultat est issu de l’ancienne requête
keyword = ""
Le résultat est réel, réussi et dans le bon format, mais il est périmé. S’il est toujours autorisé à écrire l’interface utilisateur, l’état sera erroné.
Par conséquent, dans un système concurrent, « le résultat est correct » et « le résultat est valide » sont deux choses différentes. En apparence, de nombreux problèmes de page semblent être de mauvais résultats, mais en fait, il est plus proche de ne pas pouvoir juger s’ils sont encore qualifiés pour être mis en œuvre.
5. Ne vous précipitez pas pour utiliser des outils complexes en premier. La première étape consiste à fermer des tâches similaires.
Ce dont le code ci-dessus a le plus besoin, c’est d’abord de faire une chose très simple :
**Donnez à des tâches similaires une entrée unifiée. **
Par exemple, chargez d’abord la liste comme ceci :
@MainActor
final class ArticlesViewModel: ObservableObject {
@Published private(set) var state: ViewState = .idle
@Published private(set) var items: [Article] = []
@Published var selectedCategory: String = "all"
@Published var keyword: String = ""
private let repository: ArticlesRepository
private var loadTask: Task<Void, Never>?
init(repository: ArticlesRepository) {
self.repository = repository
}
func reload() {
let request = RequestContext(
category: selectedCategory,
keyword: keyword
)
loadTask?.cancel()
loadTask = Task {
await performLoad(request: request)
}
}
private func performLoad(request: RequestContext) async {
state = .loading
do {
let result = try await repository.fetchArticles(
category: request.category,
keyword: request.keyword
)
guard !Task.isCancelled else { return }
guard request.category == selectedCategory,
request.keyword == keyword else { return }
items = result
state = .loaded
} catch is CancellationError {
// 取消不更新页面
} catch {
guard !Task.isCancelled else { return }
state = .failed(error.localizedDescription)
}
}
}
Ce code fait plusieurs choses très critiques :
- Il n’y a qu’un seul point d’arrêt pour des tâches de chargement similaires
loadTask - Lorsqu’une nouvelle tâche arrive, l’ancienne tâche sera annulée en premier
- Geler le “contexte actuel” à
RequestContextlors de l’envoi d’une requête - Une fois le résultat renvoyé, il sera vérifié s’il correspond toujours à la page actuelle
Notez que ce qui est vraiment important ici, c’est que les relations entre les tâches commencent à devenir claires.
6. Le “gel du contexte de la demande” est si critique
De nombreux articles sur la concurrence parlent d’annulation de tâches, mais ne mettent pas suffisamment l’accent sur « l’instantané du contexte ». Mais dans le secteur des pages, c’est très important.
Par exemple, lorsque vous demandez :
selectedCategory = "ios"keyword = "swift"
Ensuite, ces deux valeurs ne doivent pas lire dynamiquement les dernières valeurs sur le ViewModel actuel après le vol de la requête. Sinon, vous obtiendrez souvent un état très étrange :
- Lors de l’envoi d’une requête, il s’agit d’un ensemble de paramètres
- Un autre ensemble de paramètres est utilisé lors de la vérification des résultats
Un principe très pratique est donc le suivant :
Lors du lancement d’une tâche asynchrone, figez le contexte métier dont dépend réellement la tâche.
De cette façon, nous disposerons d’une base claire pour juger plus tard « si ce résultat est toujours le résultat actuel ».
7. De nombreux bugs de concurrence aboutissent à “trop d’entrées d’écriture de statut”
Une situation courante est que lorsque vous rencontrez un problème de concurrence, vous penserez immédiatement à :
- Tu veux le verrouiller ?
- Veux-tu être acteur ?
- Voulez-vous changer de sujet ?
Bien sûr, ces problèmes sont parfois importants, mais dans les scénarios au niveau de la page, les problèmes les plus courants sont en réalité :
- Il y a trop d’endroits pour écrire
items - Trop de places peuvent être modifiées
isLoading - Trop d’entrées peuvent envoyer des demandes directement
Une fois que les entrées d’écriture d’état sont dispersées, même s’il n’y a pas de véritable concurrence entre les données, le phénomène de « la combinaison est fausse » se produira.
Ainsi, lorsque j’effectue ce type de dépannage, je pose généralement d’abord les questions suivantes :
- Quels codes ont le pouvoir de modifier ce statut
- Quelles tâches ont le droit de mettre fin au chargement en cours
- Quels résultats ont le droit d’écraser la liste actuelle
Une fois que ces problèmes ne sont pas résolus, ce n’est généralement qu’une question de temps avant que des bogues ne se développent.
8. Une séquence d’évolution plus proche du projet réel
Si vous voulez vraiment résoudre ce genre de problème, je vous suggère d’évoluer dans cet ordre au lieu d’introduire trop de mécanismes au début :
1. Fermez l’entrée aux tâches similaires
Tout d’abord, laissez le “chargement de liste” n’avoir qu’une seule entrée unifiée, au lieu d’envoyer sa propre demande pour chaque événement de l’interface utilisateur.
2. Clarifier la relation de remplacement des tâches
Quelles tâches doivent être simultanées et lesquelles doivent annuler les anciennes tâches et ne conserver que la dernière.
3. Geler le contexte de la demande
Collectez les paramètres commerciaux clés sur lesquels vous vous basez lors des requêtes dans un objet clair.
4. Add validity judgment to the result
Tous les résultats renvoyés avec succès ne sont pas éligibles pour modifier la page actuelle.
5. Enfin, envisagez un isolement d’état partagé plus complexe
Par exemple, le cache partagé entre pages, la coordination des ressources entre modules, puis examinez Actor, coordinateur unifié et d’autres solutions.
Cet ordre est plus stable car il résout d’abord la relation de concurrence métier, plutôt que d’introduire d’abord un vocabulaire technique plus complexe.
9. Conclusion : l’essence de la plupart des problèmes de concurrence métier est « l’absence de modélisation des relations entre les tâches »
Les conditions de concurrence, les demandes en double et la confusion des états semblent être trois problèmes, mais les causes profondes réelles sont souvent très proches :
- Who is the same task as who, no modeling
- De nouvelles tâches arrivent, que faire des anciennes tâches, il n’y a pas de modélisation
- Is the result still valid? Il n’y a pas de modélisation.
- Où puis-je écrire mon statut sans le fermer ?
Alors pour reformuler cet article de manière plus courte, je dirais :
La plupart des problèmes de concurrence dans les entreprises semblent incompétents avec la syntaxe de concurrence, mais en fait, ils sont plus proches de l’échec à modéliser clairement les relations entre les tâches, la validité des résultats et les autorisations d’écriture des états.
Une fois que ces trois choses commenceront à devenir claires, de nombreuses « confusions accidentelles » disparaîtront plus facilement que vous ne le pensez.
What to read next
Want more posts about Swift Concurrency?
Posts in the same category are usually the best next step for reading more on this topic.
View same categoryWant to keep following #Swift Concurrency?
Tags are useful for related tools, specific problems, and similar troubleshooting notes.
View same tagWant to explore another direction?
If you are not sure what to read next, return to the homepage and start from categories, topics, or latest updates.
Back home