Utilisez une seule OperationQueue de Foundation à longue durée de vie et définissez son maxConcurrentOperationCount avant de soumettre les calculs. Pour un utilitaire Mac qui reçoit une requête à chaque appui sur un bouton, cela limite le travail simultané tout en permettant aux requêtes suivantes d’attendre.
Ce tutoriel construit le moteur de calcul pour cette tâche sur ordinateur et un programme autonome permettant d’en vérifier le comportement. Chaque requête contient des entiers synthétiques, et chaque résultat reste en mémoire. Le calcul s’exécute de manière synchrone dans son opération, de sorte que la durée de vie de l’opération couvre le travail effectif.
Quel outil de concurrence convient aux requêtes individuelles entrantes ?
Une OperationQueue accepte les opérations individuellement, et sa méthode addOperation(_:), fondée sur un bloc, encapsule chaque fermeture soumise dans une opération. Conservez la file dans un objet qui persiste d’une requête à l’autre, puis soumettez une fermeture pour chaque élément entrant. La documentation d’Apple sur addOperation(_:) décrit cette méthode de soumission.
Définissez maxConcurrentOperationCount sur une limite positive, par exemple 2. Apple définit cette propriété comme le nombre maximal d’opérations de cette file pouvant s’exécuter simultanément. La limite s’applique à cette file particulière ; créer une file distincte pour chaque requête annulerait l’effet d’une limite partagée. La réduire par la suite ne met pas fin aux opérations déjà en cours. La documentation d’Apple sur la limite de concurrence explique ces restrictions.
Un groupe de tâches Swift a une durée de vie différente. Il appartient à une portée withTaskGroup, ne peut pas être utilisé depuis l’extérieur de la tâche qui l’a créé et attend la fin de ses tâches enfants avant de retourner. Par conséquent, conserver un groupe de tâches comme propriété pour de futurs appuis sur un bouton sans lien entre eux n’est pas pris en charge. Pour ce moteur de calcul synchrone, une file d’opérations conservée fournit la durée de vie nécessaire et un réglage explicite de la concurrence. La documentation de la bibliothèque standard Swift sur les groupes de tâches décrit ces restrictions.
| Organisation du travail | Approche adaptée |
|---|---|
| Des calculs synchrones indépendants arrivent individuellement | Conservez une OperationQueue et soumettez une opération par calcul. |
| Une opération asynchrone parente gère un ensemble fini de tâches enfants | Utilisez un groupe de tâches dans la portée de cette opération parente ; contrôlez explicitement les admissions si une limite numérique est requise. |
| Le travail doit mettre à jour l’interface de l’application de bureau | Effectuez le calcul séparément, puis transmettez son résultat à du code isolé sur MainActor. |
La dernière ligne concerne la transmission du résultat. Le guide du langage Swift désigne l’acteur principal comme l’espace réservé à l’état de l’interface et montre des fermetures explicitement isolées sur l’acteur principal. Gardez les calculs coûteux en dehors de cette étape de transmission. Le langage de programmation Swift : concurrence explique l’isolation sur l’acteur principal.
Définir le calcul et le contrat d’achèvement
L’exemple accepte un identifiant entier et un nombre d’itérations compris entre 1 et 1_000_000. Pour un nombre d’itérations de n, il additionne les carrés de tous les entiers de 1 à n. Il renvoie l’identifiant avec un résultat UInt64. L’identifiant étiquette la sortie ; il n’influe pas sur le calcul arithmétique.
Ces limites sont données à titre d’exemple et ont été choisies pour ce programme. Pour l’entrée maximale acceptée, le résultat est 333333833333500000, une valeur qui tient dans un UInt64. La validation a lieu avant la soumission : une requête invalide déclenche donc immédiatement une erreur et ne crée aucune opération en attente. La restriction des entrées garantit également la validité de l’intervalle de boucle.
Chaque requête acceptée produit un résultat stocké à la fin de son opération. Les résultats s’accumulent dans un tableau ; des identifiants répétés n’écrasent donc pas les résultats précédents. La démonstration utilise des identifiants distincts en ordre croissant et trie les résultats par identifiant pour faciliter la lecture ; cet ordre d’affichage ne prétend pas refléter l’ordre d’exécution.
Ce contrat suppose que la file reste disponible et que ses opérations s’exécutent jusqu’au bout. L’exemple n’expose pas l’annulation. Si vous l’ajoutez par la suite, il faudra prévoir un résultat distinct pour les requêtes inachevées, car Apple décrit l’annulation comme une demande d’arrêt à laquelle le travail en cours doit répondre. La documentation d’Apple sur OperationQueue décrit la durée de vie et l’annulation des opérations.
Construire un programme complet avec une limite partagée
Utilisez un Mac équipé d’un compilateur Swift 6.2 ou plus récent et du SDK macOS fourni avec Xcode ou ses outils en ligne de commande. Enregistrez le programme complet ci-dessous sous le nom main.swift. Les commandes ci-dessous sélectionnent explicitement une isolation par défaut non isolée, afin que les fonctions auxiliaires de calcul soient indépendantes des paramètres d’isolation de la cible de l’application. Swift décrit ce paramètre du compilateur dans SE-0466 : contrôler l’inférence de l’isolation par défaut des acteurs.
Le journal des résultats utilise NSLock pour protéger à la fois son tableau et ses compteurs d’activité. Sa conformité @unchecked Sendable confie cette responsabilité de synchronisation à l’implémentation : chaque accès à l’état mutable prend le même verrou. Apple décrit cette responsabilité dans Sendable et la protection des sections critiques dans NSLock.
import Foundation
enum InputError: Error {
case invalidIterations
}
struct Calculation: Sendable {
let id: Int
let iterations: Int
}
func sumOfSquares(_ job: Calculation) -> UInt64 {
var sum: UInt64 = 0
for integer in 1...job.iterations {
let value = UInt64(integer)
sum += value * value
}
return sum
}
final class ExecutionLog: @unchecked Sendable {
private let lock = NSLock()
private var active = 0
private var peak = 0
private var rows: [(id: Int, value: UInt64)] = []
func begin() {
lock.lock()
defer { lock.unlock() }
active += 1
peak = max(peak, active)
}
func finish(id: Int, value: UInt64) {
lock.lock()
defer { lock.unlock() }
rows.append((id: id, value: value))
active -= 1
}
func snapshot() -> (
rows: [(id: Int, value: UInt64)], peak: Int
) {
lock.lock()
defer { lock.unlock() }
return (rows, peak)
}
}
final class CalculationPool {
private let queue = OperationQueue()
private let log = ExecutionLog()
init(limit: Int) {
precondition(limit > 0)
queue.name = "Synthetic calculations"
queue.maxConcurrentOperationCount = limit
}
func submit(id: Int, iterations: Int) throws {
guard (1...1_000_000).contains(iterations) else {
throw InputError.invalidIterations
}
let job = Calculation(id: id, iterations: iterations)
queue.addOperation { [log] in
log.begin()
let value = sumOfSquares(job)
log.finish(id: job.id, value: value)
}
}
// Call only after the producer has stopped submitting work.
// This blocking method is for the command-line demonstration.
func finishForDemo() -> (
rows: [(id: Int, value: UInt64)], peak: Int
) {
queue.waitUntilAllOperationsAreFinished()
return log.snapshot()
}
}
@main
struct Demo {
static func main() throws {
let limit = 2
let pool = CalculationPool(limit: limit)
try pool.submit(id: 1, iterations: 100_000)
try pool.submit(id: 2, iterations: 200_000)
try pool.submit(id: 3, iterations: 300_000)
let report = pool.finishForDemo()
let rows = report.rows.sorted { $0.id < $1.id }
precondition(rows.map { $0.id } == [1, 2, 3])
precondition(rows.map { $0.value } == [
UInt64(333338333350000),
UInt64(2666686666700000),
UInt64(9000045000050000)
])
precondition(report.peak >= 1 && report.peak <= limit)
for row in rows {
print("Job \(row.id): \(row.value)")
}
print("Observed peak: \(report.peak)")
}
}
Compilez et exécutez le programme depuis le répertoire contenant main.swift :
swiftc -swift-version 6 -default-isolation nonisolated \
-parse-as-library main.swift -o calculation-demo
./calculation-demo
Les trois lignes de résultat devraient être les suivantes :
Job 1: 333338333350000
Job 2: 2666686666700000
Job 3: 9000045000050000
La dernière ligne indique un pic observé de 1 ou de 2. Une limite autorise le chevauchement ; elle ne l’impose pas. Les sommes attendues ont été vérifiées indépendamment à l’aide de l’arithmétique entière et de la formule n * (n + 1) * (2 * n + 1) / 6. Le programme Swift a été examiné, mais n’a pas pu être compilé dans l’environnement de recherche, car aucun exécutable Swift n’y était installé.
Pourquoi cette limite couvre-t-elle le calcul effectif ?
Chaque opération démarre son compteur d’activité, effectue l’intégralité du calcul, enregistre son résultat, puis retourne. Aucun calcul asynchrone supplémentaire ne s’échappe de cette fermeture. Ainsi, limiter les opérations en cours limite également ces calculs.
Conservez ce lien lorsque vous adaptez le programme. Si une opération se contente de créer une Task distincte puis retourne immédiatement, la file considère l’opération terminée alors que cette tâche peut continuer à calculer. La limite des opérations régirait alors les courtes fermetures de lancement. Cela découle de la durée de vie documentée des opérations et de l’API de soumission par bloc. La présentation d’Apple sur OperationQueue et addOperation(_:) définissent ces éléments.
Le verrou du journal ne protège que de courtes sections de gestion. Le garder pendant sumOfSquares(_:) sérialiserait l’accès au calcul et empêcherait le chevauchement recherché. Chaque acquisition et libération du verrou reste dans une même méthode synchrone, conformément à l’exigence d’Apple selon laquelle un NSLock doit être déverrouillé sur le thread qui l’a verrouillé. La documentation d’Apple sur NSLock explique cette exigence.
Pour vérifier une adaptation, exécutez les trois mêmes requêtes avec limit réglé sur 1, puis rétablissez 2. Les deux exécutions doivent conserver les trois résultats exacts. L’exécution en série devrait indiquer un pic de 1 ; la seconde doit rester sous 2. Ces vérifications portent sur la conservation des résultats et la limite observée des calculs, sans supposer un ordre d’exécution particulier.
Relier les actions entrantes de l’application de bureau sans bloquer l’interface
Conservez une seule CalculationPool dans le composant qui gère la fonctionnalité de calcul de l’application de bureau. Chaque action entrante appelle submit(id:iterations:) une fois. Évitez de recréer le pool dans le gestionnaire d’action, car chaque nouvelle instance possède une file et une limite indépendantes.
La démonstration appelle finishForDemo() uniquement après la dernière soumission, afin que le processus en ligne de commande puisse recueillir les résultats avant de se terminer. Cette méthode bloque le thread qui l’appelle. Dans une application de bureau, utilisez une transmission individuelle des résultats au lieu de l’appeler depuis un gestionnaire de bouton ou depuis l’acteur principal. Apple décrit explicitement le comportement bloquant de waitUntilAllOperationsAreFinished().
Pour l’intégration à l’interface, étendez l’API de soumission en lui ajoutant un rappel de résultat isolé sur MainActor et marqué @Sendable. Après le calcul d’un résultat dans l’opération, planifiez une Task avec une fermeture explicitement annotée @MainActor pour appeler ce rappel. Capturez la valeur de résultat immuable et le rappel. Limitez la mise à jour de l’affichage à cette fermeture ; laissez le calcul arithmétique dans l’opération. Swift décrit à la fois la syntaxe des fermetures sur l’acteur principal et les captures des fermetures Sendable.
Cette version pour l’interface nécessite de préciser davantage la définition de l’achèvement : terminer le calcul et afficher son résultat sont deux événements distincts. Le programme en ligne de commande attend la fin des calculs et le stockage synchrone des résultats. Un rappel d’interface planifié séparément peut s’exécuter plus tard.
Que se passe-t-il lorsque les soumissions dépassent la capacité de calcul ?
Le paramètre de concurrence limite les opérations en cours, tandis que les travaux soumis en supplément peuvent rester en attente. Il ne limite pas non plus la croissance du tableau des résultats terminés de l’exemple. Cet exemple impose donc une limite à la concurrence des calculs, mais ne limite ni la mémoire totale ni la file d’attente. Apple précise que la file conserve les opérations jusqu’à leur achèvement. La présentation d’OperationQueue décrit cette conservation.
Si la fonctionnalité de bureau peut générer des pointes prolongées, choisissez une politique d’admission avant de l’étendre : n’acceptez qu’un nombre fixe de requêtes en attente et signalez les refus, mettez en pause les soumissions ultérieures, ou remplacez un aperçu synthétique en attente par une requête plus récente si ce comportement est explicitement souhaité. Coordonnez les compteurs d’admission et les mises à jour d’achèvement au moyen d’un seul propriétaire synchronisé. La limite des travaux en cours ne peut, à elle seule, appliquer ces politiques.
Pour les requêtes synchrones individuelles, la conception essentielle est simple : conservez une file, configurez une fois sa limite, gardez chaque calcul dans son opération et synchronisez les résultats partagés. Préservez ces principes lorsque vous reliez les actions de l’application de bureau, puis ajoutez une politique explicite de gestion de la file d’attente uniquement lorsque le rythme des arrivées l’exige.
Sources
- OperationQueue — Apple.
- maxConcurrentOperationCount — Apple.
- addOperation(_:) — Apple.
- waitUntilAllOperationsAreFinished() — Apple.
- NSLock — Apple.
- Sendable — Apple.
- Concurrence — Le langage de programmation Swift — Swift.org.
- TaskGroup.swift — code source et documentation de la bibliothèque standard Swift — Swift.org.
- SE-0466 : contrôler l’inférence de l’isolation par défaut des acteurs — Swift.org.