) } .branch { stem in stem.chain(loadWebResource =<< 'dataprofile.txt') + stem.chain(loadWebResource =<< 'imagedata.dat') } .chain(decodeImage) .chain(dewarpAndCleanupImage) .chain { completion(

Accès concurrentiel avancé dans Swift avec HoneyBee



Concevoir, tester et maintenir des algorithmes simultanés dans Rapide est difficile et obtenir les bons détails est essentiel au succès de votre application. Un algorithme simultané (également appelé programmation parallèle) est un algorithme conçu pour effectuer plusieurs (peut-être plusieurs) opérations en même temps afin de tirer parti de davantage de ressources matérielles et de réduire le temps d'exécution global.

Sur les plates-formes d'Apple, la manière traditionnelle d'écrire des algorithmes simultanés est NSOperation . La conception de NSOperation invite le programmeur à subdiviser un algorithme simultané en tâches asynchrones individuelles de longue durée. Chaque tâche serait définie dans sa propre sous-classe de NSOperation et les instances de ces classes seraient combinées via une API objective pour créer un ordre partiel des tâches au moment de l'exécution. Cette méthode de conception d'algorithmes simultanés était à la pointe de la technologie sur les plates-formes d'Apple pendant sept ans.



En 2014, Apple a introduit Expédition Grand Central (GCD) comme une avancée spectaculaire dans l'expression d'opérations simultanées. GCD, ainsi que les nouveaux blocs de fonctionnalités de langage qui l'accompagnaient et l'alimentaient, offraient un moyen de décrire de manière compacte un gestionnaire de réponse asynchrone immédiatement après la demande asynchrone de lancement. Les programmeurs n'étaient plus encouragés à diffuser la définition des tâches simultanées sur plusieurs fichiers dans de nombreuses sous-classes NSOperation. Désormais, tout un algorithme simultané pourrait être écrit dans une seule méthode. Cette augmentation de l'expressivité et de la sécurité des caractères représentait un changement conceptuel important. Un algorithme typique de cette façon d'écrire pourrait ressembler à ceci:



func processImageData(completion: (result: Image?, error: Error?) -> Void) { loadWebResource('dataprofile.txt') { (dataResource, error) in guard let dataResource = dataResource else { completion(nil, error) return } loadWebResource('imagedata.dat') { (imageResource, error) in guard let imageResource = imageResource else { completion(nil, error) return } decodeImage(dataResource, imageResource) { (imageTmp, error) in guard let imageTmp = imageTmp else { completion(nil, error) return } dewarpAndCleanupImage(imageTmp) { imageResult in guard let imageResult = imageResult else { completion(nil, error) return } completion(imageResult, nil) } } } } }

Décrivons un peu cet algorithme. La fonction processImageData est une fonction asynchrone qui effectue elle-même quatre appels asynchrones pour terminer son travail. Les quatre invocations asynchrones sont imbriquées les unes dans les autres de la manière la plus naturelle pour la gestion asynchrone basée sur des blocs. Les blocs de résultats ont chacun un paramètre d'erreur facultatif et tous sauf un contiennent un paramètre facultatif supplémentaire indiquant le résultat de l'opération aysnc.



La forme du bloc de code ci-dessus semble probablement familière à la plupart des développeurs Swift. Mais quel est le problème avec cette approche? La liste suivante de points douloureux sera probablement également familière.

Comment pouvons-nous faire mieux? Abeille est une bibliothèque de futurs / promesses qui rend la programmation simultanée Swift facile, expressive et sûre. Réécrivons l'algorithme asynchrone ci-dessus avec HoneyBee et examinons le résultat:



func processImageData(completion: (result: Image?, error: Error?) -> Void) { HoneyBee.start() .setErrorHandler { completion(nil, $0) } .branch { stem in stem.chain(loadWebResource =<< 'dataprofile.txt') + stem.chain(loadWebResource =<< 'imagedata.dat') } .chain(decodeImage) .chain(dewarpAndCleanupImage) .chain { completion($0, nil) } }

La première ligne que cette implémentation commence est une nouvelle recette HoneyBee. La deuxième ligne établit le gestionnaire d'erreurs par défaut. La gestion des erreurs n'est pas facultative dans les recettes HoneyBee. Si quelque chose ne va pas, l'algorithme doit le gérer. La troisième ligne ouvre une branche qui permet une exécution parallèle. Les deux chaînes de loadWebResource s'exécutera en parallèle et leurs résultats seront combinés (ligne 5). Les valeurs combinées des deux ressources chargées sont transmises à decodeImage et ainsi de suite le long de la chaîne jusqu'à ce que la complétion soit invoquée.

Passons en revue la liste des points faibles ci-dessus et voyons comment HoneyBee a amélioré ce code. Le maintien de cette fonction est désormais beaucoup plus facile. La recette HoneyBee ressemble à l'algorithme qu'elle exprime. Le code est lisible, compréhensible et rapidement modifiable. La conception d'HoneyBee garantit que tout mauvais ordre des instructions entraîne une erreur de compilation et non une erreur d'exécution. La fonction est désormais beaucoup moins sensible aux bugs et aux erreurs humaines.



Toutes les erreurs d'exécution possibles ont été entièrement gérées. Chaque signature de fonction prise en charge par HoneyBee (il y en a 38) est assurée d'être entièrement gérée. Dans notre exemple, le rappel à deux paramètres de style Objective-C produira soit une erreur non nulle qui sera acheminée vers le gestionnaire d'erreurs, soit une valeur non nulle qui progressera le long de la chaîne, ou bien si les deux les valeurs sont nulles HoneyBee générera une erreur expliquant que le rappel de fonction ne remplit pas son contrat.

HoneyBee gère également l'exactitude contractuelle du nombre de fois que les rappels de fonction sont appelés. Si une fonction ne parvient pas à appeler son rappel, HoneyBee produit un échec descriptif. Si la fonction appelle son rappel plus d'une fois, HoneyBee supprimera les appels auxiliaires et consignera les avertissements. Ces deux réponses aux pannes (et d’autres) peuvent être personnalisées en fonction des besoins individuels du programmeur.



Heureusement, il devrait déjà être évident que cette forme de processImageData parallélise correctement les téléchargements de ressources pour fournir des performances optimales. L’un des objectifs de conception les plus forts de HoneyBee est que la recette ressemble à l’algorithme qu’elle exprime.

Bien mieux. Droite? Mais HoneyBee a bien plus à offrir.



Soyez averti: la prochaine étude de cas n'est pas pour les faibles de cœur. Considérez la description du problème suivante: Votre application mobile utilise CoreData pour maintenir son état. Vous avez un NSManagedObject modèle appelé Media, qui représente un élément multimédia téléchargé sur votre serveur principal. L'utilisateur doit être autorisé à sélectionner des dizaines d'éléments multimédias à la fois et à les télécharger par lots sur le système backend. Les médias sont d'abord représentés via une chaîne de référence, qui doit être convertie en objet Media. Heureusement, votre application contient déjà une méthode d'assistance qui fait exactement cela:

func export(_ mediaRef: String, completion: @escaping (Media?, Error?) -> Void) { // transcoding stuff completion(Media(context: managedObjectContext), nil) }

Une fois la référence multimédia convertie en objet Media, vous devez télécharger l'élément multimédia vers le back-end. Encore une fois, vous avez une fonction d'assistance prête à faire les choses réseau.



func upload(_ media: Media, completion: @escaping (Error?) -> Void) { // network stuff completion(nil) }

Étant donné que l'utilisateur est autorisé à sélectionner des dizaines d'éléments multimédias à la fois, le concepteur UX a spécifié une quantité assez robuste de commentaires sur la progression du téléchargement. Les exigences ont été réparties dans les quatre fonctions suivantes:

/// Called if anything goes wrong in the upload func errorHandler(_ error: Error) { // do the right thing } /// Called once per mediaRef, after either a successful or unsuccessful upload func singleUploadCompletion(_ mediaRef: String) { // update a progress indicator } /// Called once per successful upload func singleUploadSuccess(_ media: Media) { // do celebratory things } /// Called if the entire batch was considered to be uploaded successfully. func totalProcessSuccess() { // declare victory }

Cependant, étant donné que votre application recherche des références multimédias qui sont parfois expirées, les responsables commerciaux ont décidé d'envoyer à l'utilisateur un message de «réussite» si au moins la moitié des téléchargements réussissent. Autrement dit, le processus concurrent doit déclarer la victoire - et appeler totalProcessSuccess - si moins de la moitié des tentatives de téléchargement échouent. Il s'agit de la spécification qui vous a été remise en tant que développeur. Mais en tant que programmeur expérimenté, vous réalisez que d'autres exigences doivent être appliquées.

Bien sûr, Business souhaite que le téléchargement par lots se fasse le plus rapidement possible, il est donc hors de question de télécharger en série. Les téléchargements doivent être effectués en parallèle.

Mais pas trop. Si vous n'avez pas de discrimination async le lot entier, les dizaines de téléchargements simultanés inonderont le NIC mobile (carte d'interface réseau), et les téléchargements se dérouleront en fait plus lentement qu'en série, pas plus vite.

Les connexions au réseau mobile ne sont pas considérées comme stables. Même de courtes transactions peuvent échouer en raison uniquement de modifications de la connectivité réseau. Afin de véritablement déclarer l'échec d'un téléversement, nous devons réessayer l'importation au moins une fois.

La stratégie de nouvelle tentative ne doit pas inclure l'opération d'exportation car elle n'est pas sujette à des échecs temporaires.

Le processus d'exportation est lié au calcul et doit donc être exécuté hors du thread principal.

Étant donné que l'exportation est liée au calcul, elle doit avoir un plus petit nombre d'instances simultanées que le reste du processus de téléchargement pour éviter de détruire le processeur.

Les quatre fonctions de rappel décrites ci-dessus mettent toutes à jour l'interface utilisateur et doivent donc toutes être appelées sur le thread principal.

Le média est un NSManagedObject, qui provient d'un NSManagedObjectContext et a ses propres exigences de filetage qui doivent être respectées.

Cette spécification de problème semble-t-elle un peu obscure? Ne soyez pas surpris si vous constatez que des problèmes comme celui-ci se cachent dans votre avenir. J'en ai rencontré un comme celui-ci dans mon propre travail. Essayons d'abord de résoudre ce problème avec des outils traditionnels. Bouclez votre ceinture, ce ne sera pas joli.

/// An enum describing specific problems that the algorithm might encounter. enum UploadingError : Error { case invalidResponse case tooManyFailures } /// A semaphore to prevent flooding the NIC let outerLimit = DispatchSemaphore(value: 4) /// A semaphore to prevent thrashing the processor let exportLimit = DispatchSemaphore(value: 1) /// The number of times to retry the upload if it fails let uploadRetries = 1 /// Dispatch group to keep track of when the entire process is finished let fullProcessDispatchGroup = DispatchGroup() /// How many of the uploads fully completed. var uploadSuccesses = 0 // this notify block is called when the full process has completed. fullProcessDispatchGroup.notify(queue: DispatchQueue.main) { let successRate = Float(uploadSuccesses) / Float(mediaReferences.count) if successRate > 0.5 { totalProcessSuccess() } else { errorHandler(UploadingError.tooManyFailures) } } // start in the background DispatchQueue.global().async { for mediaRef in mediaReferences { // alert the group that we're starting a process fullProcessDispatchGroup.enter() // wait until it's safe to start uploading outerLimit.wait() /// common cleanup operations needed later func finalizeMediaRef() { singleUploadCompletion(mediaRef) fullProcessDispatchGroup.leave() outerLimit.signal() } // wait until it's safe to start exporting exportLimit.wait() export(mediaRef) { (media, error) in // allow another export to begin exportLimit.signal() if let error = error { DispatchQueue.main.async { errorHandler(error) finalizeMediaRef() } } else { guard let media = media else { DispatchQueue.main.async { errorHandler(UploadingError.invalidResponse) finalizeMediaRef() } return } // the export was successful var uploadAttempts = 0 /// define the upload process and its retry behavior func doUpload() { // respect Media's threading requirements managedObjectContext.perform { upload(media) { error in if let error = error { if uploadAttempts

Woah! Sans commentaires, cela fait environ 75 lignes. Avez-vous suivi le raisonnement tout au long? Comment vous sentiriez-vous si vous rencontriez ce monstre lors de la première semaine d'un nouvel emploi? Vous sentiriez-vous prêt à le maintenir ou à le modifier? Sauriez-vous s'il contenait des erreurs? Contient-il des erreurs?

Maintenant, considérez l'alternative HoneyBee:

HoneyBee.start(on: DispatchQueue.main) .setErrorHandler(errorHandler) .insert(mediaReferences) .setBlockPerformer(DispatchQueue.global()) .each(limit: 4, acceptableFailure: .ratio(0.5)) { elem in elem.finally { link in link.setBlockPerformer(DispatchQueue.main) .chain(singleUploadCompletion) } .limit(1) { link in link.chain(export) } .setBlockPerformer(managedObjectContext) .retry(1) { link in link.chain(upload) // subject to transient failure } .setBlockPerformer(DispatchQueue.main) .chain(singleUploadSuccess) } .setBlockPerformer(DispatchQueue.main) .drop() .chain(totalProcessSuccess)

Comment ce formulaire vous frappe-t-il? Travaillons-y morceau par morceau. Sur la première ligne, nous commençons la recette HoneyBee, en commençant par le fil principal. En commençant sur le thread principal, nous nous assurons que toutes les erreurs seront transmises à errorHandler (ligne 2) sur le thread principal. La ligne 3 insère le mediaReferences tableau dans la chaîne de processus. Ensuite, nous passons à la file d'attente globale en arrière-plan en préparation d'un certain parallélisme. À la ligne 5, nous commençons une itération parallèle sur chacun des mediaReferences. Nous limitons ce parallélisme à un maximum de 4 opérations simultanées. Nous déclarons également que l'itération complète sera considérée comme réussie si au moins la moitié des sous-chaînes réussit (ne pas faire d'erreur). La ligne 6 déclare a finally lien qui sera appelé si la sous-chaîne ci-dessous réussit ou échoue. Sur le finally lien, nous passons au fil de discussion principal (ligne 7) et appelons singleUploadCompletion (ligne 8). Sur la ligne 10, nous définissons une parallélisation maximale de 1 (exécution unique) autour de l'opération d'exportation (ligne 11). La ligne 13 passe à la file d'attente privée appartenant à notre managedObjectContext exemple. La ligne 14 déclare une seule nouvelle tentative pour l'opération de téléchargement (ligne 15). La ligne 17 passe à nouveau au thread principal et 18 appelle singleUploadSuccess. Au moment où la ligne de temps 20 serait exécutée, toutes les itérations parallèles sont terminées. Si moins de la moitié des itérations échouent, la ligne 20 passe une dernière fois à la file d'attente principale (rappelez-vous que chacune a été exécutée sur la file d'attente d'arrière-plan), 21 supprime la valeur entrante (toujours mediaReferences) et 22 appelle totalProcessSuccess.

Le formulaire HoneyBee est plus clair, plus propre et plus facile à lire, sans parler de plus facile à entretenir. Qu'arriverait-il à la forme longue de cet algorithme si la boucle était nécessaire pour réintégrer les objets Media dans un tableau comme une fonction de carte? Une fois le changement effectué, dans quelle mesure seriez-vous certain que toutes les exigences de l'algorithme sont toujours satisfaites? Dans la forme HoneyBee, ce changement consisterait à remplacer chacun par une carte pour utiliser une fonction de carte parallèle. (Oui, il a réduit aussi.)

HoneyBee est une puissante bibliothèque de futurs pour Swift qui rend l'écriture d'algorithmes asynchrones et simultanés plus facile, plus sûre et plus expressive. Dans cet article, nous avons vu comment HoneyBee peut rendre vos algorithmes plus faciles à maintenir, plus corrects et plus rapides. HoneyBee prend également en charge d'autres paradigmes asynchrones clés tels que la prise en charge des nouvelles tentatives, plusieurs gestionnaires d'erreurs, la protection des ressources et le traitement de la collecte (formes asynchrones de mappage, de filtrage et de réduction). Pour une liste complète des fonctionnalités, reportez-vous au site Internet . Pour en savoir plus ou poser des questions, voir le tout nouveau Forums communautaires .

Annexe: Garantie de l'exactitude contractuelle des fonctions asynchrones

Assurer l'exactitude contractuelle des fonctions est un principe fondamental de l'informatique. À tel point que pratiquement tous les compilateurs modernes ont des vérifications pour s'assurer qu'une fonction qui déclare retourner une valeur, retourne exactement une fois. Renvoyer moins d'une ou plus d'une fois est traité comme une erreur et empêche de manière appropriée une compilation complète.

Mais cette assistance du compilateur ne s'applique généralement pas aux fonctions asynchrones. Prenons l'exemple (ludique) suivant:

func generateIcecream(from int: Int, completion: (String) -> Void) { if int > 5 { if int <20 { completion('Chocolate') } else if int < 10 { completion('Strawberry') } completion('Pistachio') } else if int < 2 { completion('Vanilla') } }

Le generateIcecream La fonction accepte un Int et renvoie une chaîne de manière asynchrone. Le compilateur Swift accepte volontiers la forme ci-dessus comme correcte, même si elle contient des problèmes évidents. Étant donné certaines entrées, cette fonction peut appeler la complétion zéro, une ou deux fois. Les programmeurs qui ont travaillé avec des fonctions asynchrones se rappelleront souvent des exemples de ce problème dans leur propre travail. Que pouvons-nous faire? Certes, nous pourrions refactoriser le code pour qu'il soit plus net (un commutateur avec des cas de plage fonctionnerait ici). Mais parfois, la complexité fonctionnelle est difficile à réduire. Ne serait-il pas préférable que le compilateur nous aide à vérifier l'exactitude, comme il le fait avec des fonctions qui retournent régulièrement?

Il s'avère qu'il existe un moyen. Observez l'incantation Swifty suivante:

func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int <20 { completion('Chocolate') } else if int < 10 { completion('Strawberry') } // else completion('Pistachio') } else if int < 2 { completion('Vanilla') } }

Les quatre lignes insérées en haut de cette fonction forcent le compilateur à vérifier que le rappel d'achèvement est appelé exactement une fois, ce qui signifie que cette fonction ne se compile plus. Ce qui se passe? Dans la première ligne, nous déclarons mais n'initialisons pas le résultat que nous voulons finalement que cette fonction produise. En le laissant indéfini, nous nous assurons qu'il doit être attribué une fois avant de pouvoir être utilisé, et en le déclarant, nous nous assurons qu'il ne peut jamais être attribué à deux fois. La deuxième ligne est un différé qui s'exécutera comme action finale de cette fonction. Il appelle le bloc de complétion avec finalResult - après avoir été affecté par le reste de la fonction. La ligne 3 crée une nouvelle constante appelée complétion qui masque le paramètre de rappel. La nouvelle complétion est de type Void qui ne déclare aucune API publique. Cette ligne garantit que toute utilisation de la complétion après cette ligne sera une erreur du compilateur. Le report sur la ligne 2 est la seule utilisation autorisée du bloc de complétion. La ligne 4 supprime un avertissement du compilateur qui serait autrement présent sur la nouvelle constante de complétion non utilisée.

Nous avons donc réussi à forcer le compilateur swift à signaler que cette fonction asynchrone ne remplit pas son contrat. Passons en revue les étapes pour le corriger. Tout d'abord, remplaçons tous les accès directs au rappel par une attribution à finalResult.

func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int <20 { finalResult = 'Chocolate' } else if int < 10 { finalResult = 'Strawberry' } // else finalResult = 'Pistachio' } else if int < 2 { finalResult = 'Vanilla' } }

Maintenant, le compilateur signale deux problèmes:

error: AsyncCorrectness.playground:1:8: error: constant 'finalResult' used before being initialized defer { completion(finalResult) } ^ error: AsyncCorrectness.playground:11:3: error: immutable value 'finalResult' may only be initialized once finalResult = 'Pistachio'

Comme prévu, la fonction a un chemin où finalResult est attribué zéro fois et également un chemin où il est attribué plus d'une fois. Nous résolvons ces problèmes comme suit:

func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int <20 { finalResult = 'Chocolate' } else if int < 10 { finalResult = 'Strawberry' } else { finalResult = 'Pistachio' } } else if int < 2 { finalResult = 'Vanilla' } else { finalResult = 'Neapolitan' } }

La «pistache» a été déplacée vers une clause else appropriée et nous nous rendons compte que nous n'avons pas couvert le cas général - qui est bien sûr «napolitain».

Les modèles qui viennent d'être décrits peuvent être facilement ajustés pour renvoyer des valeurs facultatives, des erreurs facultatives ou des types complexes comme l'énumération Result commune. En contraignant le compilateur à vérifier que les rappels sont invoqués une seule fois, nous pouvons affirmer l'exactitude et l'exhaustivité des fonctions asynchrones.

Comprendre les bases

Qu'est-ce que la concurrence dans la programmation?

Un algorithme simultané (également appelé programmation parallèle) est un algorithme conçu pour effectuer plusieurs (peut-être plusieurs) opérations en même temps afin de tirer parti de davantage de ressources matérielles et de réduire le temps d'exécution global.

Quels sont les problèmes de concurrence?

La forme de «pyramide de malheur» des blocs de code imbriqués peut rapidement devenir difficile à manier, la gestion des erreurs asynchrones peut être peu intuitive ou incomplète, les problèmes avec les intégrations tierces sont exacerbés et, bien que destinés à des gains de performances, ils peuvent entraîner du gaspillage et des sous-optimaux performance.

, nil) } }

La première ligne que cette implémentation commence est une nouvelle recette HoneyBee. La deuxième ligne établit le gestionnaire d'erreurs par défaut. La gestion des erreurs n'est pas facultative dans les recettes HoneyBee. Si quelque chose ne va pas, l'algorithme doit le gérer. La troisième ligne ouvre une branche qui permet une exécution parallèle. Les deux chaînes de loadWebResource s'exécutera en parallèle et leurs résultats seront combinés (ligne 5). Les valeurs combinées des deux ressources chargées sont transmises à decodeImage et ainsi de suite le long de la chaîne jusqu'à ce que la complétion soit invoquée.

Passons en revue la liste des points faibles ci-dessus et voyons comment HoneyBee a amélioré ce code. Le maintien de cette fonction est désormais beaucoup plus facile. La recette HoneyBee ressemble à l'algorithme qu'elle exprime. Le code est lisible, compréhensible et rapidement modifiable. La conception d'HoneyBee garantit que tout mauvais ordre des instructions entraîne une erreur de compilation et non une erreur d'exécution. La fonction est désormais beaucoup moins sensible aux bugs et aux erreurs humaines.

Toutes les erreurs d'exécution possibles ont été entièrement gérées. Chaque signature de fonction prise en charge par HoneyBee (il y en a 38) est assurée d'être entièrement gérée. Dans notre exemple, le rappel à deux paramètres de style Objective-C produira soit une erreur non nulle qui sera acheminée vers le gestionnaire d'erreurs, soit une valeur non nulle qui progressera le long de la chaîne, ou bien si les deux les valeurs sont nulles HoneyBee générera une erreur expliquant que le rappel de fonction ne remplit pas son contrat.

HoneyBee gère également l'exactitude contractuelle du nombre de fois que les rappels de fonction sont appelés. Si une fonction ne parvient pas à appeler son rappel, HoneyBee produit un échec descriptif. Si la fonction appelle son rappel plus d'une fois, HoneyBee supprimera les appels auxiliaires et consignera les avertissements. Ces deux réponses aux pannes (et d’autres) peuvent être personnalisées en fonction des besoins individuels du programmeur.

Heureusement, il devrait déjà être évident que cette forme de processImageData parallélise correctement les téléchargements de ressources pour fournir des performances optimales. L’un des objectifs de conception les plus forts de HoneyBee est que la recette ressemble à l’algorithme qu’elle exprime.

Bien mieux. Droite? Mais HoneyBee a bien plus à offrir.

Soyez averti: la prochaine étude de cas n'est pas pour les faibles de cœur. Considérez la description du problème suivante: Votre application mobile utilise CoreData pour maintenir son état. Vous avez un NSManagedObject modèle appelé Media, qui représente un élément multimédia téléchargé sur votre serveur principal. L'utilisateur doit être autorisé à sélectionner des dizaines d'éléments multimédias à la fois et à les télécharger par lots sur le système backend. Les médias sont d'abord représentés via une chaîne de référence, qui doit être convertie en objet Media. Heureusement, votre application contient déjà une méthode d'assistance qui fait exactement cela:

func export(_ mediaRef: String, completion: @escaping (Media?, Error?) -> Void) { // transcoding stuff completion(Media(context: managedObjectContext), nil) }

Une fois la référence multimédia convertie en objet Media, vous devez télécharger l'élément multimédia vers le back-end. Encore une fois, vous avez une fonction d'assistance prête à faire les choses réseau.

comment implémenter bootstrap en html
func upload(_ media: Media, completion: @escaping (Error?) -> Void) { // network stuff completion(nil) }

Étant donné que l'utilisateur est autorisé à sélectionner des dizaines d'éléments multimédias à la fois, le concepteur UX a spécifié une quantité assez robuste de commentaires sur la progression du téléchargement. Les exigences ont été réparties dans les quatre fonctions suivantes:

/// Called if anything goes wrong in the upload func errorHandler(_ error: Error) { // do the right thing } /// Called once per mediaRef, after either a successful or unsuccessful upload func singleUploadCompletion(_ mediaRef: String) { // update a progress indicator } /// Called once per successful upload func singleUploadSuccess(_ media: Media) { // do celebratory things } /// Called if the entire batch was considered to be uploaded successfully. func totalProcessSuccess() { // declare victory }

Cependant, étant donné que votre application recherche des références multimédias qui sont parfois expirées, les responsables commerciaux ont décidé d'envoyer à l'utilisateur un message de «réussite» si au moins la moitié des téléchargements réussissent. Autrement dit, le processus concurrent doit déclarer la victoire - et appeler totalProcessSuccess - si moins de la moitié des tentatives de téléchargement échouent. Il s'agit de la spécification qui vous a été remise en tant que développeur. Mais en tant que programmeur expérimenté, vous réalisez que d'autres exigences doivent être appliquées.

Bien sûr, Business souhaite que le téléchargement par lots se fasse le plus rapidement possible, il est donc hors de question de télécharger en série. Les téléchargements doivent être effectués en parallèle.

Mais pas trop. Si vous n'avez pas de discrimination async le lot entier, les dizaines de téléchargements simultanés inonderont le NIC mobile (carte d'interface réseau), et les téléchargements se dérouleront en fait plus lentement qu'en série, pas plus vite.

Les connexions au réseau mobile ne sont pas considérées comme stables. Même de courtes transactions peuvent échouer en raison uniquement de modifications de la connectivité réseau. Afin de véritablement déclarer l'échec d'un téléversement, nous devons réessayer l'importation au moins une fois.

La stratégie de nouvelle tentative ne doit pas inclure l'opération d'exportation car elle n'est pas sujette à des échecs temporaires.

comment pirater une carte de debit

Le processus d'exportation est lié au calcul et doit donc être exécuté hors du thread principal.

Étant donné que l'exportation est liée au calcul, elle doit avoir un plus petit nombre d'instances simultanées que le reste du processus de téléchargement pour éviter de détruire le processeur.

Les quatre fonctions de rappel décrites ci-dessus mettent toutes à jour l'interface utilisateur et doivent donc toutes être appelées sur le thread principal.

Le média est un NSManagedObject, qui provient d'un NSManagedObjectContext et a ses propres exigences de filetage qui doivent être respectées.

Cette spécification de problème semble-t-elle un peu obscure? Ne soyez pas surpris si vous constatez que des problèmes comme celui-ci se cachent dans votre avenir. J'en ai rencontré un comme celui-ci dans mon propre travail. Essayons d'abord de résoudre ce problème avec des outils traditionnels. Bouclez votre ceinture, ce ne sera pas joli.

/// An enum describing specific problems that the algorithm might encounter. enum UploadingError : Error { case invalidResponse case tooManyFailures } /// A semaphore to prevent flooding the NIC let outerLimit = DispatchSemaphore(value: 4) /// A semaphore to prevent thrashing the processor let exportLimit = DispatchSemaphore(value: 1) /// The number of times to retry the upload if it fails let uploadRetries = 1 /// Dispatch group to keep track of when the entire process is finished let fullProcessDispatchGroup = DispatchGroup() /// How many of the uploads fully completed. var uploadSuccesses = 0 // this notify block is called when the full process has completed. fullProcessDispatchGroup.notify(queue: DispatchQueue.main) { let successRate = Float(uploadSuccesses) / Float(mediaReferences.count) if successRate > 0.5 { totalProcessSuccess() } else { errorHandler(UploadingError.tooManyFailures) } } // start in the background DispatchQueue.global().async { for mediaRef in mediaReferences { // alert the group that we're starting a process fullProcessDispatchGroup.enter() // wait until it's safe to start uploading outerLimit.wait() /// common cleanup operations needed later func finalizeMediaRef() { singleUploadCompletion(mediaRef) fullProcessDispatchGroup.leave() outerLimit.signal() } // wait until it's safe to start exporting exportLimit.wait() export(mediaRef) { (media, error) in // allow another export to begin exportLimit.signal() if let error = error { DispatchQueue.main.async { errorHandler(error) finalizeMediaRef() } } else { guard let media = media else { DispatchQueue.main.async { errorHandler(UploadingError.invalidResponse) finalizeMediaRef() } return } // the export was successful var uploadAttempts = 0 /// define the upload process and its retry behavior func doUpload() { // respect Media's threading requirements managedObjectContext.perform { upload(media) { error in if let error = error { if uploadAttempts

Woah! Sans commentaires, cela fait environ 75 lignes. Avez-vous suivi le raisonnement tout au long? Comment vous sentiriez-vous si vous rencontriez ce monstre lors de la première semaine d'un nouvel emploi? Vous sentiriez-vous prêt à le maintenir ou à le modifier? Sauriez-vous s'il contenait des erreurs? Contient-il des erreurs?

Maintenant, considérez l'alternative HoneyBee:

HoneyBee.start(on: DispatchQueue.main) .setErrorHandler(errorHandler) .insert(mediaReferences) .setBlockPerformer(DispatchQueue.global()) .each(limit: 4, acceptableFailure: .ratio(0.5)) { elem in elem.finally { link in link.setBlockPerformer(DispatchQueue.main) .chain(singleUploadCompletion) } .limit(1) { link in link.chain(export) } .setBlockPerformer(managedObjectContext) .retry(1) { link in link.chain(upload) // subject to transient failure } .setBlockPerformer(DispatchQueue.main) .chain(singleUploadSuccess) } .setBlockPerformer(DispatchQueue.main) .drop() .chain(totalProcessSuccess)

Comment ce formulaire vous frappe-t-il? Travaillons-y morceau par morceau. Sur la première ligne, nous commençons la recette HoneyBee, en commençant par le fil principal. En commençant sur le thread principal, nous nous assurons que toutes les erreurs seront transmises à errorHandler (ligne 2) sur le thread principal. La ligne 3 insère le mediaReferences tableau dans la chaîne de processus. Ensuite, nous passons à la file d'attente globale en arrière-plan en préparation d'un certain parallélisme. À la ligne 5, nous commençons une itération parallèle sur chacun des mediaReferences. Nous limitons ce parallélisme à un maximum de 4 opérations simultanées. Nous déclarons également que l'itération complète sera considérée comme réussie si au moins la moitié des sous-chaînes réussit (ne pas faire d'erreur). La ligne 6 déclare a finally lien qui sera appelé si la sous-chaîne ci-dessous réussit ou échoue. Sur le finally lien, nous passons au fil de discussion principal (ligne 7) et appelons singleUploadCompletion (ligne 8). Sur la ligne 10, nous définissons une parallélisation maximale de 1 (exécution unique) autour de l'opération d'exportation (ligne 11). La ligne 13 passe à la file d'attente privée appartenant à notre managedObjectContext exemple. La ligne 14 déclare une seule nouvelle tentative pour l'opération de téléchargement (ligne 15). La ligne 17 passe à nouveau au thread principal et 18 appelle singleUploadSuccess. Au moment où la ligne de temps 20 serait exécutée, toutes les itérations parallèles sont terminées. Si moins de la moitié des itérations échouent, la ligne 20 passe une dernière fois à la file d'attente principale (rappelez-vous que chacune a été exécutée sur la file d'attente d'arrière-plan), 21 supprime la valeur entrante (toujours mediaReferences) et 22 appelle totalProcessSuccess.

Le formulaire HoneyBee est plus clair, plus propre et plus facile à lire, sans parler de plus facile à entretenir. Qu'arriverait-il à la forme longue de cet algorithme si la boucle était nécessaire pour réintégrer les objets Media dans un tableau comme une fonction de carte? Une fois le changement effectué, dans quelle mesure seriez-vous certain que toutes les exigences de l'algorithme sont toujours satisfaites? Dans la forme HoneyBee, ce changement consisterait à remplacer chacun par une carte pour utiliser une fonction de carte parallèle. (Oui, il a réduit aussi.)

HoneyBee est une puissante bibliothèque de futurs pour Swift qui rend l'écriture d'algorithmes asynchrones et simultanés plus facile, plus sûre et plus expressive. Dans cet article, nous avons vu comment HoneyBee peut rendre vos algorithmes plus faciles à maintenir, plus corrects et plus rapides. HoneyBee prend également en charge d'autres paradigmes asynchrones clés tels que la prise en charge des nouvelles tentatives, plusieurs gestionnaires d'erreurs, la protection des ressources et le traitement de la collecte (formes asynchrones de mappage, de filtrage et de réduction). Pour une liste complète des fonctionnalités, reportez-vous au site Internet . Pour en savoir plus ou poser des questions, voir le tout nouveau Forums communautaires .

Annexe: Garantie de l'exactitude contractuelle des fonctions asynchrones

Assurer l'exactitude contractuelle des fonctions est un principe fondamental de l'informatique. À tel point que pratiquement tous les compilateurs modernes ont des vérifications pour s'assurer qu'une fonction qui déclare retourner une valeur, retourne exactement une fois. Renvoyer moins d'une ou plus d'une fois est traité comme une erreur et empêche de manière appropriée une compilation complète.

Mais cette assistance du compilateur ne s'applique généralement pas aux fonctions asynchrones. Prenons l'exemple (ludique) suivant:

func generateIcecream(from int: Int, completion: (String) -> Void) { if int > 5 { if int <20 { completion('Chocolate') } else if int < 10 { completion('Strawberry') } completion('Pistachio') } else if int < 2 { completion('Vanilla') } }

Le generateIcecream La fonction accepte un Int et renvoie une chaîne de manière asynchrone. Le compilateur Swift accepte volontiers la forme ci-dessus comme correcte, même si elle contient des problèmes évidents. Étant donné certaines entrées, cette fonction peut appeler la complétion zéro, une ou deux fois. Les programmeurs qui ont travaillé avec des fonctions asynchrones se rappelleront souvent des exemples de ce problème dans leur propre travail. Que pouvons-nous faire? Certes, nous pourrions refactoriser le code pour qu'il soit plus net (un commutateur avec des cas de plage fonctionnerait ici). Mais parfois, la complexité fonctionnelle est difficile à réduire. Ne serait-il pas préférable que le compilateur nous aide à vérifier l'exactitude, comme il le fait avec des fonctions qui retournent régulièrement?

Il s'avère qu'il existe un moyen. Observez l'incantation Swifty suivante:

func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int <20 { completion('Chocolate') } else if int < 10 { completion('Strawberry') } // else completion('Pistachio') } else if int < 2 { completion('Vanilla') } }

Les quatre lignes insérées en haut de cette fonction forcent le compilateur à vérifier que le rappel d'achèvement est appelé exactement une fois, ce qui signifie que cette fonction ne se compile plus. Ce qui se passe? Dans la première ligne, nous déclarons mais n'initialisons pas le résultat que nous voulons finalement que cette fonction produise. En le laissant indéfini, nous nous assurons qu'il doit être attribué une fois avant de pouvoir être utilisé, et en le déclarant, nous nous assurons qu'il ne peut jamais être attribué à deux fois. La deuxième ligne est un différé qui s'exécutera comme action finale de cette fonction. Il appelle le bloc de complétion avec finalResult - après avoir été affecté par le reste de la fonction. La ligne 3 crée une nouvelle constante appelée complétion qui masque le paramètre de rappel. La nouvelle complétion est de type Void qui ne déclare aucune API publique. Cette ligne garantit que toute utilisation de la complétion après cette ligne sera une erreur du compilateur. Le report sur la ligne 2 est la seule utilisation autorisée du bloc de complétion. La ligne 4 supprime un avertissement du compilateur qui serait autrement présent sur la nouvelle constante de complétion non utilisée.

Nous avons donc réussi à forcer le compilateur swift à signaler que cette fonction asynchrone ne remplit pas son contrat. Passons en revue les étapes pour le corriger. Tout d'abord, remplaçons tous les accès directs au rappel par une attribution à finalResult.

func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int <20 { finalResult = 'Chocolate' } else if int < 10 { finalResult = 'Strawberry' } // else finalResult = 'Pistachio' } else if int < 2 { finalResult = 'Vanilla' } }

Maintenant, le compilateur signale deux problèmes:

error: AsyncCorrectness.playground:1:8: error: constant 'finalResult' used before being initialized defer { completion(finalResult) } ^ error: AsyncCorrectness.playground:11:3: error: immutable value 'finalResult' may only be initialized once finalResult = 'Pistachio'

Comme prévu, la fonction a un chemin où finalResult est attribué zéro fois et également un chemin où il est attribué plus d'une fois. Nous résolvons ces problèmes comme suit:

func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int <20 { finalResult = 'Chocolate' } else if int < 10 { finalResult = 'Strawberry' } else { finalResult = 'Pistachio' } } else if int < 2 { finalResult = 'Vanilla' } else { finalResult = 'Neapolitan' } }

La «pistache» a été déplacée vers une clause else appropriée et nous nous rendons compte que nous n'avons pas couvert le cas général - qui est bien sûr «napolitain».

Les modèles qui viennent d'être décrits peuvent être facilement ajustés pour renvoyer des valeurs facultatives, des erreurs facultatives ou des types complexes comme l'énumération Result commune. En contraignant le compilateur à vérifier que les rappels sont invoqués une seule fois, nous pouvons affirmer l'exactitude et l'exhaustivité des fonctions asynchrones.

De quels éléments un concepteur doit-il tenir compte lors de la planification de la conception d'un document ?

Comprendre les bases

Qu'est-ce que la concurrence dans la programmation?

Un algorithme simultané (également appelé programmation parallèle) est un algorithme conçu pour effectuer plusieurs (peut-être plusieurs) opérations en même temps afin de tirer parti de davantage de ressources matérielles et de réduire le temps d'exécution global.

Quels sont les problèmes de concurrence?

La forme de «pyramide de malheur» des blocs de code imbriqués peut rapidement devenir difficile à manier, la gestion des erreurs asynchrones peut être peu intuitive ou incomplète, les problèmes avec les intégrations tierces sont exacerbés et, bien que destinés à des gains de performances, ils peuvent entraîner du gaspillage et des sous-optimaux performance.