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

Advanced Concurrency στο Swift με το HoneyBee



Σχεδιασμός, δοκιμή και συντήρηση ταυτόχρονων αλγορίθμων στο Ταχύς είναι δύσκολο και η σωστή ενημέρωση είναι κρίσιμη για την επιτυχία της εφαρμογής σας. Ένας παράλληλος αλγόριθμος (που ονομάζεται επίσης παράλληλος προγραμματισμός) είναι ένας αλγόριθμος που έχει σχεδιαστεί για να εκτελεί ταυτόχρονα πολλαπλές (ίσως πολλές) λειτουργίες για να εκμεταλλευτεί περισσότερους πόρους υλικού και να μειώσει τον συνολικό χρόνο εκτέλεσης.

Στις πλατφόρμες της Apple, ο παραδοσιακός τρόπος σύνταξης ταυτόχρονων αλγορίθμων είναι NS Λειτουργία . Ο σχεδιασμός του NSOperation καλεί τον προγραμματιστή να υποδιαιρέσει έναν ταυτόχρονο αλγόριθμο σε μεμονωμένες, μακροχρόνιες, ασύγχρονες εργασίες. Κάθε εργασία θα οριζόταν στη δική της υποκατηγορία του NSOperation και οι περιπτώσεις αυτών των τάξεων θα συνδυάζονταν μέσω ενός αντικειμενικού API για να δημιουργήσουν μια μερική σειρά εργασιών στο χρόνο εκτέλεσης. Αυτή η μέθοδος σχεδιασμού ταυτόχρονων αλγορίθμων ήταν η τελευταία λέξη της τεχνολογίας στις πλατφόρμες της Apple για επτά χρόνια.



Το 2014 η Apple παρουσίασε Αποστολή Grand Central (GCD) ως ένα δραματικό βήμα προς τα εμπρός στην έκφραση των ταυτόχρονων λειτουργιών. Το GCD, μαζί με τα νέα μπλοκ χαρακτηριστικών γλωσσών που το συνόδευσαν και το τροφοδότησαν, παρείχαν έναν τρόπο για να περιγράψουν συνοπτικά έναν ασύγχρονο χειριστή απόκρισης αμέσως μετά την έναρξη του αιτήματος ασύγχυσης. Δεν ενθαρρύνθηκαν πλέον οι προγραμματιστές να διαδώσουν τον ορισμό των ταυτόχρονων εργασιών σε πολλά αρχεία σε πολλές υποκατηγορίες NSOperation. Τώρα, ένας ολόκληρος αλγόριθμος ταυτόχρονα θα μπορούσε να γραφτεί με μία μόνο μέθοδο. Αυτή η αύξηση της εκφραστικότητας και της ασφάλειας τύπου ήταν μια σημαντική εννοιολογική αλλαγή προς τα εμπρός. Ένας τυπικός αλγόριθμος αυτού του τρόπου γραφής μπορεί να μοιάζει με τον ακόλουθο:



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) } } } } }

Ας σπάσουμε αυτόν τον αλγόριθμο λίγο. Η συνάρτηση processImageData είναι μια ασύγχρονη συνάρτηση που πραγματοποιεί τέσσερις ασύγχρονες κλήσεις για να ολοκληρώσει την εργασία της. Οι τέσσερις επίκληση ασύγχυσης τοποθετούνται το ένα μέσα στο άλλο με τον τρόπο που είναι πιο φυσικό για τον χειρισμό ασύγχρονης βάσης. Το μπλοκ αποτελεσμάτων έχει το καθένα μια προαιρετική παράμετρο σφάλματος και όλα εκτός από ένα περιέχουν μια πρόσθετη προαιρετική παράμετρο που υποδηλώνει το αποτέλεσμα της λειτουργίας aysnc.



Το σχήμα του παραπάνω μπλοκ κώδικα φαίνεται πιθανώς οικείο στους περισσότερους προγραμματιστές Swift. Αλλά τι συμβαίνει με αυτήν την προσέγγιση; Η ακόλουθη λίστα σημείων πόνου θα είναι πιθανώς εξίσου οικεία.

Πώς μπορούμε να κάνουμε καλύτερα; Μέλισσα είναι μια βιβλιοθήκη μελλοντικών / υποσχέσεων που καθιστά τον ταυτόχρονο προγραμματισμό Swift εύκολο, εκφραστικό και ασφαλές. Ας ξαναγράψουμε τον παραπάνω αλγόριθμο async με το HoneyBee και εξετάσουμε το αποτέλεσμα:



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) } }

Η πρώτη γραμμή που ξεκινά αυτή η εφαρμογή είναι μια νέα συνταγή HoneyBee. Η δεύτερη γραμμή καθορίζει τον προεπιλεγμένο χειριστή σφαλμάτων. Ο χειρισμός σφαλμάτων δεν είναι προαιρετικός στις συνταγές HoneyBee. Εάν κάτι μπορεί να πάει στραβά, ο αλγόριθμος πρέπει να το χειριστεί. Η τρίτη γραμμή ανοίγει έναν κλάδο που επιτρέπει παράλληλη εκτέλεση. Οι δύο αλυσίδες του loadWebResource θα εκτελεστεί παράλληλα και τα αποτελέσματά τους θα συνδυαστούν (γραμμή 5). Οι συνδυασμένες τιμές των δύο φορτωμένων πόρων προωθούνται στο decodeImage και ούτω καθεξής στην αλυσίδα έως ότου καλείται η ολοκλήρωση.

Ας δούμε την παραπάνω λίστα σημείων πόνου και να δούμε πώς η HoneyBee βελτίωσε αυτόν τον κώδικα. Η διατήρηση αυτής της λειτουργίας είναι τώρα πολύ πιο εύκολη. Η συνταγή HoneyBee μοιάζει με τον αλγόριθμο που εκφράζει. Ο κωδικός είναι αναγνώσιμος, κατανοητός και γρήγορα τροποποιήσιμος. Ο σχεδιασμός της HoneyBee διασφαλίζει ότι οποιαδήποτε εσφαλμένη διάταξη των οδηγιών οδηγεί σε σφάλμα χρόνου μεταγλώττισης και όχι σε σφάλμα χρόνου εκτέλεσης. Η λειτουργία είναι τώρα πολύ λιγότερο ευαίσθητη σε σφάλματα και ανθρώπινα λάθη.



Όλα τα πιθανά σφάλματα χρόνου εκτέλεσης έχουν αντιμετωπιστεί πλήρως. Κάθε υπογραφή λειτουργίας που υποστηρίζει η HoneyBee (υπάρχουν 38 από αυτές) διασφαλίζεται ότι θα αντιμετωπιστεί πλήρως. Στο παράδειγμά μας, η επιστροφή δύο παραμέτρων τύπου Objective-C είτε θα παράγει ένα σφάλμα μη-μηδέν που θα δρομολογηθεί στο πρόγραμμα χειρισμού σφαλμάτων, είτε θα παράγει μια τιμή μη μηδενική που θα προχωρήσει κάτω από την αλυσίδα, ή αλλιώς εάν Οι τιμές είναι μηδέν Η HoneyBee θα δημιουργήσει ένα σφάλμα που εξηγεί ότι η συνάρτηση επανάκλησης δεν πληροί τη σύμβασή της.

Το HoneyBee χειρίζεται επίσης τη συμβατική ορθότητα για τον αριθμό των φορών που επικαλούνται επιστροφές λειτουργίας. Εάν μια συνάρτηση αποτύχει να επικαλεστεί την επανάκλησή της, το HoneyBee παράγει μια περιγραφική αποτυχία. Εάν η συνάρτηση επικαλεστεί την επανάκλησή της περισσότερες από μία φορές, το HoneyBee θα καταργήσει τις επικουρικές επικλήσεις και τις προειδοποιήσεις καταγραφής. Και οι δύο αυτές απαντήσεις σφαλμάτων (και άλλες) μπορούν να προσαρμοστούν για τις ατομικές ανάγκες του προγραμματιστή.



Ας ελπίσουμε ότι θα πρέπει ήδη να είναι προφανές ότι αυτή η μορφή processImageData παραλληλίζει σωστά τις λήψεις πόρων για να παρέχει τη βέλτιστη απόδοση. Ένας από τους ισχυρότερους σχεδιαστικούς στόχους της HoneyBee είναι ότι η συνταγή πρέπει να μοιάζει με τον αλγόριθμο που εκφράζει.

Πολύ καλύτερα. Σωστά? Αλλά η HoneyBee έχει πολλά περισσότερα να προσφέρει.



Προσοχή: Η επόμενη μελέτη περίπτωσης δεν είναι για εξασθενημένη καρδιά. Εξετάστε την ακόλουθη περιγραφή προβλήματος: Η εφαρμογή σας για κινητά χρησιμοποιεί CoreData να επιμείνει στην κατάστασή του. Έχετε ένα NSManagedObject μοντέλο που ονομάζεται Media, το οποίο αντιπροσωπεύει ένα στοιχείο πολυμέσων που έχει φορτωθεί στον διακομιστή υποστήριξης. Επιτρέπεται στον χρήστη να επιλέγει δεκάδες στοιχεία πολυμέσων ταυτόχρονα και να τα ανεβάζει κατά παρτίδες στο σύστημα backend. Τα μέσα εκπροσωπούνται πρώτα μέσω συμβολοσειράς αναφοράς, η οποία πρέπει να μετατραπεί σε αντικείμενο πολυμέσων. Ευτυχώς, η εφαρμογή σας περιέχει ήδη μια βοηθητική μέθοδο που κάνει ακριβώς αυτό:

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

Αφού η αναφορά πολυμέσων μετατραπεί σε αντικείμενο πολυμέσων, πρέπει να ανεβάσετε το στοιχείο πολυμέσων στο παρασκήνιο. Και πάλι έχετε μια λειτουργία βοηθού έτοιμη να κάνει τα πράγματα του δικτύου.



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

Επειδή ο χρήστης έχει τη δυνατότητα να επιλέξει δεκάδες στοιχεία πολυμέσων ταυτόχρονα, ο σχεδιαστής UX έχει καθορίσει αρκετά ισχυρό αριθμό σχολίων σχετικά με την πρόοδο της μεταφόρτωσης. Οι απαιτήσεις έχουν αποσταχθεί στις ακόλουθες τέσσερις λειτουργίες:

/// 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 }

Ωστόσο, επειδή η εφαρμογή σας προβαίνει σε αναφορές πολυμέσων που μερικές φορές λήγουν, οι διαχειριστές επιχείρησης αποφάσισαν να στείλουν στον χρήστη ένα μήνυμα «επιτυχίας» εάν τουλάχιστον οι μισές από τις μεταφορτώσεις είναι επιτυχημένες. Δηλαδή, ότι η ταυτόχρονη διαδικασία πρέπει να δηλώσει τη νίκη - και να καλέσει totalProcessSuccess - εάν αποτύχουν λιγότερες από τις μισές απόπειρες μεταφόρτωσης. Αυτή είναι η προδιαγραφή που σας έχει δοθεί ως προγραμματιστής. Αλλά ως έμπειρος προγραμματιστής, συνειδητοποιείτε ότι υπάρχουν περισσότερες απαιτήσεις που πρέπει να εφαρμοστούν.

Φυσικά, η Επιχείρηση θέλει η παρτίδα μεταφόρτωση να πραγματοποιείται το συντομότερο δυνατό, οπότε η σειριακή μεταφόρτωση είναι άσκοπη. Οι μεταφορτώσεις πρέπει να εκτελούνται παράλληλα.

Αλλά όχι τόσο πολύ. Εάν απλώς αδιάκριτα async ολόκληρη η παρτίδα, οι δεκάδες ταυτόχρονες μεταφορτώσεις θα πλημμυρίσουν το κινητό NIC (κάρτα διασύνδεσης δικτύου) και οι μεταφορτώσεις θα προχωρήσουν πιο αργά από ό, τι σειριακά, όχι πιο γρήγορα.

Οι συνδέσεις δικτύου κινητής τηλεφωνίας δεν θεωρούνται σταθερές. Ακόμη και οι σύντομες συναλλαγές ενδέχεται να αποτύχουν μόνο λόγω αλλαγών στη συνδεσιμότητα δικτύου. Για να δηλώσουμε πραγματικά ότι μια μεταφόρτωση απέτυχε, θα πρέπει να επαναλάβουμε τη μεταφόρτωση τουλάχιστον μία φορά.

Η πολιτική επανάληψης δεν πρέπει να περιλαμβάνει τη λειτουργία εξαγωγής, επειδή δεν υπόκειται σε παροδικές αποτυχίες.

Η διαδικασία εξαγωγής είναι δεσμευμένη σε υπολογιστές και επομένως πρέπει να εκτελεστεί εκτός του βασικού νήματος.

Επειδή η εξαγωγή είναι δεσμευμένη σε υπολογιστές, θα πρέπει να έχει μικρότερο αριθμό ταυτόχρονων παρουσιών από την υπόλοιπη διαδικασία μεταφόρτωσης για να αποφευχθεί η καταστολή του επεξεργαστή.

Οι τέσσερις λειτουργίες επανάκλησης που περιγράφονται παραπάνω ενημερώνουν το περιβάλλον εργασίας χρήστη και έτσι πρέπει να κληθούν όλες στο κύριο νήμα.

Τα μέσα είναι ένα NSManagedObject, που προέρχεται από ένα NSManagedObjectContext και έχει τις δικές του απαιτήσεις σπειρώματος που πρέπει να τηρούνται.

Μήπως αυτή η προδιαγραφή προβλήματος φαίνεται λίγο ασαφής; Μην εκπλαγείτε αν βρείτε προβλήματα όπως αυτό που κρύβονται στο μέλλον σας. Συνάντησα κάτι τέτοιο στη δική μου δουλειά. Ας προσπαθήσουμε πρώτα να λύσουμε αυτό το πρόβλημα με παραδοσιακά εργαλεία. Ανεβείτε, αυτό δεν θα είναι όμορφο.

/// 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

Ωχ! Χωρίς σχόλια, αυτό είναι περίπου 75 γραμμές. Ακολουθήσατε τη συλλογιστική μέχρι το τέλος; Πώς θα νιώθατε αν συναντήσατε αυτό το τέρας την πρώτη σας εβδομάδα σε μια νέα δουλειά; Θα θέλατε να το διατηρήσετε ή να το τροποποιήσετε; Γνωρίζατε εάν περιείχε λάθη; Περιέχει σφάλματα;

Τώρα, σκεφτείτε την εναλλακτική λύση του 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)

Πώς σας εμφανίζει αυτή η φόρμα; Ας το δουλέψουμε κομμάτι-κομμάτι. Στην πρώτη γραμμή, ξεκινάμε τη συνταγή HoneyBee, ξεκινώντας από το κύριο νήμα. Ξεκινώντας από το κύριο νήμα διασφαλίζουμε ότι όλα τα σφάλματα θα μεταβιβαστούν στο errorHandler (γραμμή 2) στο κύριο νήμα. Η γραμμή 3 εισάγει το mediaReferences συστοιχία στην αλυσίδα διαδικασίας. Στη συνέχεια, μεταβαίνουμε στην παγκόσμια ουρά παρασκηνίου για την προετοιμασία κάποιου παραλληλισμού. Στη γραμμή 5, ξεκινάμε μια παράλληλη επανάληψη σε καθένα από τα mediaReferences. Περιορίζουμε αυτόν τον παραλληλισμό σε έως 4 ταυτόχρονες λειτουργίες. Δηλώνουμε επίσης ότι η πλήρης επανάληψη θα θεωρείται επιτυχής εάν τουλάχιστον οι μισές από τις υποκατηγορίες επιτύχουν (μην κάνετε λάθος). Η γραμμή 6 δηλώνει a finally σύνδεσμος που θα κληθεί αν η παρακάτω αλυσίδα επιτυγχάνει ή αποτύχει. Στο finally σύνδεση, μεταβαίνουμε στο κύριο νήμα (γραμμή 7) και καλούμε singleUploadCompletion (γραμμή 8). Στη γραμμή 10, ορίσαμε έναν μέγιστο παραλληλισμό 1 (μεμονωμένη εκτέλεση) γύρω από τη λειτουργία εξαγωγής (γραμμή 11). Η γραμμή 13 μεταβαίνει στην ιδιωτική ουρά που ανήκει στο managedObjectContext παράδειγμα. Η γραμμή 14 δηλώνει μία προσπάθεια επανάληψης για τη λειτουργία μεταφόρτωσης (γραμμή 15). Η γραμμή 17 αλλάζει ξανά στο κύριο νήμα και 18 επικαλείται singleUploadSuccess. Μέχρι τη γραμμή 20 θα εκτελεστεί, όλες οι παράλληλες επαναλήψεις έχουν ολοκληρωθεί. Εάν αποτύχουν λιγότερες από τις μισές επαναλήψεις, τότε η γραμμή 20 μεταβαίνει στην κύρια ουρά για τελευταία φορά (θυμηθείτε ότι η καθεμία εκτελέστηκε στην ουρά παρασκηνίου), 21 πέσει η εισερχόμενη τιμή (ακόμα mediaReferences) και 22 επικαλείται totalProcessSuccess.

Η φόρμα HoneyBee είναι πιο καθαρή, πιο καθαρή και πιο ευανάγνωστη, για να μην αναφέρουμε ευκολότερα τη συντήρηση. Τι θα συνέβαινε στη μακροχρόνια μορφή αυτού του αλγορίθμου εάν απαιτείται ο βρόχος για την επανένταξη των αντικειμένων πολυμέσων σε έναν πίνακα όπως μια λειτουργία χάρτη; Αφού κάνατε την αλλαγή, πόσο σίγουροι θα ήσασταν ότι όλες οι απαιτήσεις του αλγορίθμου εξακολουθούσαν να πληρούνται; Στη φόρμα HoneyBee, αυτή η αλλαγή θα ήταν να αντικατασταθεί το καθένα με χάρτη για να χρησιμοποιηθεί μια παράλληλη λειτουργία χάρτη. (Ναι, έχει μειωθεί επίσης.)

Το HoneyBee είναι μια ισχυρή βιβλιοθήκη μελλοντικής εκπλήρωσης για το Swift που κάνει τη γραφή ασύγχρονοι και ταυτόχρονοι αλγόριθμοι ευκολότεροι, ασφαλέστεροι και πιο εκφραστικοί. Σε αυτό το άρθρο, έχουμε δει πώς η HoneyBee μπορεί να διευκολύνει τη συντήρηση των αλγορίθμων σας, να είναι πιο σωστή και πιο γρήγορη. Το HoneyBee έχει επίσης υποστήριξη για άλλα βασικά παραδείγματα ασύγχρονου, όπως δοκιμή επανάληψης, πολλαπλοί χειριστές σφαλμάτων, προστασία πόρων και επεξεργασία συλλογής (μορφές χάρτη, φίλτρο και μείωση ασύγχρονων). Για μια πλήρη λίστα χαρακτηριστικών ανατρέξτε στο δικτυακός τόπος . Για να μάθετε περισσότερα ή να κάνετε ερωτήσεις δείτε το ολοκαίνουργιο κοινοτικά φόρουμ .

Παράρτημα: Διασφάλιση της συμβατικής ορθότητας των λειτουργιών Async

Η διασφάλιση της συμβατικής ορθότητας των λειτουργιών αποτελεί θεμελιώδες δόγμα της επιστήμης των υπολογιστών. Τόσο πολύ που σχεδόν όλοι οι σύγχρονοι μεταγλωττιστές έχουν ελέγχους για να διασφαλίσουν ότι μια συνάρτηση που δηλώνει ότι επιστρέφει μια τιμή, επιστρέφει ακριβώς μία φορά. Η επιστροφή λιγότερη ή περισσότερες από μία φορές αντιμετωπίζεται ως σφάλμα και αποτρέπει κατάλληλα μια πλήρη συλλογή.

Αλλά αυτή η βοήθεια μεταγλωττιστή συνήθως δεν ισχύει για ασύγχρονες λειτουργίες. Εξετάστε το ακόλουθο (παιχνιδιάρικο) παράδειγμα:

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') } }

Το generateIcecream Η συνάρτηση δέχεται ένα Int και επιστρέφει ασύγχρονα ένα String. Ο γρήγορος μεταγλωττιστής αποδέχεται με χαρά την παραπάνω φόρμα ως σωστή, παρόλο που περιέχει ορισμένα προφανή προβλήματα. Δεδομένων ορισμένων εισόδων, αυτή η συνάρτηση μπορεί να καλέσει την ολοκλήρωση μηδέν, μία ή δύο φορές. Οι προγραμματιστές που έχουν εργαστεί με λειτουργίες async συχνά θα θυμούνται παραδείγματα αυτού του προβλήματος στη δουλειά τους. Τι μπορούμε να κάνουμε? Σίγουρα, θα μπορούσαμε να αναδιαμορφώσουμε τον κώδικα ώστε να είναι πιο καθαρός (ένας διακόπτης με θήκες εύρους θα λειτουργούσε εδώ). Αλλά μερικές φορές η λειτουργική πολυπλοκότητα είναι δύσκολο να μειωθεί. Δεν θα ήταν καλύτερο αν ο μεταγλωττιστής θα μπορούσε να μας βοηθήσει στην επαλήθευση της ορθότητας όπως συμβαίνει με τις τακτικές επιστροφές λειτουργιών;

Αποδεικνύεται ότι υπάρχει τρόπος. Παρατηρήστε την ακόλουθη μαγεία Swifty:

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') } }

Οι τέσσερις γραμμές που εισάγονται στο πάνω μέρος αυτής της συνάρτησης αναγκάζουν τον μεταγλωττιστή να επαληθεύσει ότι η επιστροφή κλήσης ολοκλήρωσης καλείται ακριβώς μία φορά, πράγμα που σημαίνει ότι αυτή η συνάρτηση δεν συντάσσεται πλέον. Τι συμβαίνει? Στην πρώτη γραμμή, δηλώνουμε αλλά δεν αρχικοποιούμε το αποτέλεσμα που τελικά θέλουμε να παράγει αυτή η συνάρτηση. Αφήνοντάς το απροσδιόριστο, διασφαλίζουμε ότι πρέπει να αντιστοιχιστεί μία φορά πριν μπορεί να χρησιμοποιηθεί, και δηλώνοντάς το ας διασφαλίσουμε ότι δεν μπορεί να αντιστοιχιστεί ποτέ σε δύο φορές. Η δεύτερη γραμμή είναι μια αναβολή που θα εκτελεστεί ως η τελική ενέργεια αυτής της λειτουργίας. Επικαλείται το μπλοκ ολοκλήρωσης με finalResult - αφού έχει ανατεθεί από την υπόλοιπη λειτουργία. Η γραμμή 3 δημιουργεί μια νέα σταθερά που ονομάζεται ολοκλήρωση που σκιάζει την παράμετρο επανάκλησης. Η νέα ολοκλήρωση είναι τύπου Void που δεν δηλώνει δημόσιο API. Αυτή η γραμμή διασφαλίζει ότι οποιαδήποτε χρήση ολοκλήρωσης μετά από αυτήν τη γραμμή θα είναι σφάλμα μεταγλωττιστή. Η αναβολή στη γραμμή 2 είναι η μόνη επιτρεπόμενη χρήση του μπλοκ ολοκλήρωσης. Η γραμμή 4 καταργεί μια προειδοποίηση μεταγλωττιστή που διαφορετικά θα υπήρχε σχετικά με τη χρήση της νέας σταθεράς ολοκλήρωσης.

Αναγκάσαμε λοιπόν με επιτυχία το πρόγραμμα γρήγορης μεταγλώττισης να αναφέρει ότι αυτή η ασύγχρονη λειτουργία δεν εκπληρώνει τη σύμβασή της. Ας ακολουθήσουμε τα βήματα για να το κάνουμε σωστό. Αρχικά, ας αντικαταστήσουμε όλη την άμεση πρόσβαση στην επιστροφή κλήσης με μια ανάθεση σε 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' } }

Τώρα ο μεταγλωττιστής αναφέρει δύο προβλήματα:

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'

Όπως αναμενόταν, η συνάρτηση έχει ένα μονοπάτι όπου 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' } else { finalResult = 'Neapolitan' } }

Το «Φιστίκι» έχει μετακινηθεί σε μια κατάλληλη ρήτρα και συνειδητοποιούμε ότι δεν καταφέραμε να καλύψουμε τη γενική υπόθεση - που φυσικά είναι «Ναπολιτάν».

Τα μοτίβα που μόλις περιγράφηκαν μπορούν εύκολα να προσαρμοστούν ώστε να επιστρέφουν προαιρετικές τιμές, προαιρετικά σφάλματα ή σύνθετους τύπους όπως το κοινό αποτέλεσμα. Αναγκάζοντας τον μεταγλωττιστή να επαληθεύσει ότι οι επιστροφές κλήσεων πραγματοποιούνται ακριβώς μία φορά, μπορούμε να επιβεβαιώσουμε την ορθότητα και την πληρότητα των ασύγχρονων συναρτήσεων.

Κατανόηση των βασικών

Τι είναι το ταυτόχρονο στον προγραμματισμό;

Ένας παράλληλος αλγόριθμος (που ονομάζεται επίσης παράλληλος προγραμματισμός) είναι ένας αλγόριθμος που έχει σχεδιαστεί για να εκτελεί ταυτόχρονα πολλαπλές (ίσως πολλές) λειτουργίες για να εκμεταλλευτεί περισσότερους πόρους υλικού και να μειώσει τον συνολικό χρόνο εκτέλεσης.

Ποια είναι τα προβλήματα της ταυτόχρονης;

Το σχήμα 'πυραμίδα της καταστροφής' των ένθετων μπλοκ κώδικα μπορεί γρήγορα να γίνει δυσκίνητο, καθώς ο ασύγχρονος χειρισμός σφαλμάτων μπορεί να είναι ακούσιος ή ελλιπής, ζητήματα με ενσωματώσεις τρίτων επιδεινώνονται και ενώ προορίζονται για κέρδη απόδοσης, μπορεί να οδηγήσουν σε σπατάλη και υπο-βέλτιστο εκτέλεση.

, nil) } }

Η πρώτη γραμμή που ξεκινά αυτή η εφαρμογή είναι μια νέα συνταγή HoneyBee. Η δεύτερη γραμμή καθορίζει τον προεπιλεγμένο χειριστή σφαλμάτων. Ο χειρισμός σφαλμάτων δεν είναι προαιρετικός στις συνταγές HoneyBee. Εάν κάτι μπορεί να πάει στραβά, ο αλγόριθμος πρέπει να το χειριστεί. Η τρίτη γραμμή ανοίγει έναν κλάδο που επιτρέπει παράλληλη εκτέλεση. Οι δύο αλυσίδες του loadWebResource θα εκτελεστεί παράλληλα και τα αποτελέσματά τους θα συνδυαστούν (γραμμή 5). Οι συνδυασμένες τιμές των δύο φορτωμένων πόρων προωθούνται στο decodeImage και ούτω καθεξής στην αλυσίδα έως ότου καλείται η ολοκλήρωση.

Ας δούμε την παραπάνω λίστα σημείων πόνου και να δούμε πώς η HoneyBee βελτίωσε αυτόν τον κώδικα. Η διατήρηση αυτής της λειτουργίας είναι τώρα πολύ πιο εύκολη. Η συνταγή HoneyBee μοιάζει με τον αλγόριθμο που εκφράζει. Ο κωδικός είναι αναγνώσιμος, κατανοητός και γρήγορα τροποποιήσιμος. Ο σχεδιασμός της HoneyBee διασφαλίζει ότι οποιαδήποτε εσφαλμένη διάταξη των οδηγιών οδηγεί σε σφάλμα χρόνου μεταγλώττισης και όχι σε σφάλμα χρόνου εκτέλεσης. Η λειτουργία είναι τώρα πολύ λιγότερο ευαίσθητη σε σφάλματα και ανθρώπινα λάθη.

Όλα τα πιθανά σφάλματα χρόνου εκτέλεσης έχουν αντιμετωπιστεί πλήρως. Κάθε υπογραφή λειτουργίας που υποστηρίζει η HoneyBee (υπάρχουν 38 από αυτές) διασφαλίζεται ότι θα αντιμετωπιστεί πλήρως. Στο παράδειγμά μας, η επιστροφή δύο παραμέτρων τύπου Objective-C είτε θα παράγει ένα σφάλμα μη-μηδέν που θα δρομολογηθεί στο πρόγραμμα χειρισμού σφαλμάτων, είτε θα παράγει μια τιμή μη μηδενική που θα προχωρήσει κάτω από την αλυσίδα, ή αλλιώς εάν Οι τιμές είναι μηδέν Η HoneyBee θα δημιουργήσει ένα σφάλμα που εξηγεί ότι η συνάρτηση επανάκλησης δεν πληροί τη σύμβασή της.

Το HoneyBee χειρίζεται επίσης τη συμβατική ορθότητα για τον αριθμό των φορών που επικαλούνται επιστροφές λειτουργίας. Εάν μια συνάρτηση αποτύχει να επικαλεστεί την επανάκλησή της, το HoneyBee παράγει μια περιγραφική αποτυχία. Εάν η συνάρτηση επικαλεστεί την επανάκλησή της περισσότερες από μία φορές, το HoneyBee θα καταργήσει τις επικουρικές επικλήσεις και τις προειδοποιήσεις καταγραφής. Και οι δύο αυτές απαντήσεις σφαλμάτων (και άλλες) μπορούν να προσαρμοστούν για τις ατομικές ανάγκες του προγραμματιστή.

Ας ελπίσουμε ότι θα πρέπει ήδη να είναι προφανές ότι αυτή η μορφή processImageData παραλληλίζει σωστά τις λήψεις πόρων για να παρέχει τη βέλτιστη απόδοση. Ένας από τους ισχυρότερους σχεδιαστικούς στόχους της HoneyBee είναι ότι η συνταγή πρέπει να μοιάζει με τον αλγόριθμο που εκφράζει.

Πολύ καλύτερα. Σωστά? Αλλά η HoneyBee έχει πολλά περισσότερα να προσφέρει.

Προσοχή: Η επόμενη μελέτη περίπτωσης δεν είναι για εξασθενημένη καρδιά. Εξετάστε την ακόλουθη περιγραφή προβλήματος: Η εφαρμογή σας για κινητά χρησιμοποιεί CoreData να επιμείνει στην κατάστασή του. Έχετε ένα NSManagedObject μοντέλο που ονομάζεται Media, το οποίο αντιπροσωπεύει ένα στοιχείο πολυμέσων που έχει φορτωθεί στον διακομιστή υποστήριξης. Επιτρέπεται στον χρήστη να επιλέγει δεκάδες στοιχεία πολυμέσων ταυτόχρονα και να τα ανεβάζει κατά παρτίδες στο σύστημα backend. Τα μέσα εκπροσωπούνται πρώτα μέσω συμβολοσειράς αναφοράς, η οποία πρέπει να μετατραπεί σε αντικείμενο πολυμέσων. Ευτυχώς, η εφαρμογή σας περιέχει ήδη μια βοηθητική μέθοδο που κάνει ακριβώς αυτό:

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

Αφού η αναφορά πολυμέσων μετατραπεί σε αντικείμενο πολυμέσων, πρέπει να ανεβάσετε το στοιχείο πολυμέσων στο παρασκήνιο. Και πάλι έχετε μια λειτουργία βοηθού έτοιμη να κάνει τα πράγματα του δικτύου.

πώς να χρησιμοποιήσετε ένα τερματικό bloomberg
func upload(_ media: Media, completion: @escaping (Error?) -> Void) { // network stuff completion(nil) }

Επειδή ο χρήστης έχει τη δυνατότητα να επιλέξει δεκάδες στοιχεία πολυμέσων ταυτόχρονα, ο σχεδιαστής UX έχει καθορίσει αρκετά ισχυρό αριθμό σχολίων σχετικά με την πρόοδο της μεταφόρτωσης. Οι απαιτήσεις έχουν αποσταχθεί στις ακόλουθες τέσσερις λειτουργίες:

/// 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 }

Ωστόσο, επειδή η εφαρμογή σας προβαίνει σε αναφορές πολυμέσων που μερικές φορές λήγουν, οι διαχειριστές επιχείρησης αποφάσισαν να στείλουν στον χρήστη ένα μήνυμα «επιτυχίας» εάν τουλάχιστον οι μισές από τις μεταφορτώσεις είναι επιτυχημένες. Δηλαδή, ότι η ταυτόχρονη διαδικασία πρέπει να δηλώσει τη νίκη - και να καλέσει totalProcessSuccess - εάν αποτύχουν λιγότερες από τις μισές απόπειρες μεταφόρτωσης. Αυτή είναι η προδιαγραφή που σας έχει δοθεί ως προγραμματιστής. Αλλά ως έμπειρος προγραμματιστής, συνειδητοποιείτε ότι υπάρχουν περισσότερες απαιτήσεις που πρέπει να εφαρμοστούν.

Φυσικά, η Επιχείρηση θέλει η παρτίδα μεταφόρτωση να πραγματοποιείται το συντομότερο δυνατό, οπότε η σειριακή μεταφόρτωση είναι άσκοπη. Οι μεταφορτώσεις πρέπει να εκτελούνται παράλληλα.

Αλλά όχι τόσο πολύ. Εάν απλώς αδιάκριτα async ολόκληρη η παρτίδα, οι δεκάδες ταυτόχρονες μεταφορτώσεις θα πλημμυρίσουν το κινητό NIC (κάρτα διασύνδεσης δικτύου) και οι μεταφορτώσεις θα προχωρήσουν πιο αργά από ό, τι σειριακά, όχι πιο γρήγορα.

Οι συνδέσεις δικτύου κινητής τηλεφωνίας δεν θεωρούνται σταθερές. Ακόμη και οι σύντομες συναλλαγές ενδέχεται να αποτύχουν μόνο λόγω αλλαγών στη συνδεσιμότητα δικτύου. Για να δηλώσουμε πραγματικά ότι μια μεταφόρτωση απέτυχε, θα πρέπει να επαναλάβουμε τη μεταφόρτωση τουλάχιστον μία φορά.

Η πολιτική επανάληψης δεν πρέπει να περιλαμβάνει τη λειτουργία εξαγωγής, επειδή δεν υπόκειται σε παροδικές αποτυχίες.

Η διαδικασία εξαγωγής είναι δεσμευμένη σε υπολογιστές και επομένως πρέπει να εκτελεστεί εκτός του βασικού νήματος.

Επειδή η εξαγωγή είναι δεσμευμένη σε υπολογιστές, θα πρέπει να έχει μικρότερο αριθμό ταυτόχρονων παρουσιών από την υπόλοιπη διαδικασία μεταφόρτωσης για να αποφευχθεί η καταστολή του επεξεργαστή.

Οι τέσσερις λειτουργίες επανάκλησης που περιγράφονται παραπάνω ενημερώνουν το περιβάλλον εργασίας χρήστη και έτσι πρέπει να κληθούν όλες στο κύριο νήμα.

Τα μέσα είναι ένα NSManagedObject, που προέρχεται από ένα NSManagedObjectContext και έχει τις δικές του απαιτήσεις σπειρώματος που πρέπει να τηρούνται.

Μήπως αυτή η προδιαγραφή προβλήματος φαίνεται λίγο ασαφής; Μην εκπλαγείτε αν βρείτε προβλήματα όπως αυτό που κρύβονται στο μέλλον σας. Συνάντησα κάτι τέτοιο στη δική μου δουλειά. Ας προσπαθήσουμε πρώτα να λύσουμε αυτό το πρόβλημα με παραδοσιακά εργαλεία. Ανεβείτε, αυτό δεν θα είναι όμορφο.

/// 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

Ωχ! Χωρίς σχόλια, αυτό είναι περίπου 75 γραμμές. Ακολουθήσατε τη συλλογιστική μέχρι το τέλος; Πώς θα νιώθατε αν συναντήσατε αυτό το τέρας την πρώτη σας εβδομάδα σε μια νέα δουλειά; Θα θέλατε να το διατηρήσετε ή να το τροποποιήσετε; Γνωρίζατε εάν περιείχε λάθη; Περιέχει σφάλματα;

Τώρα, σκεφτείτε την εναλλακτική λύση του HoneyBee:

Παράδειγμα δοκιμής μονάδας c#
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)

Πώς σας εμφανίζει αυτή η φόρμα; Ας το δουλέψουμε κομμάτι-κομμάτι. Στην πρώτη γραμμή, ξεκινάμε τη συνταγή HoneyBee, ξεκινώντας από το κύριο νήμα. Ξεκινώντας από το κύριο νήμα διασφαλίζουμε ότι όλα τα σφάλματα θα μεταβιβαστούν στο errorHandler (γραμμή 2) στο κύριο νήμα. Η γραμμή 3 εισάγει το mediaReferences συστοιχία στην αλυσίδα διαδικασίας. Στη συνέχεια, μεταβαίνουμε στην παγκόσμια ουρά παρασκηνίου για την προετοιμασία κάποιου παραλληλισμού. Στη γραμμή 5, ξεκινάμε μια παράλληλη επανάληψη σε καθένα από τα mediaReferences. Περιορίζουμε αυτόν τον παραλληλισμό σε έως 4 ταυτόχρονες λειτουργίες. Δηλώνουμε επίσης ότι η πλήρης επανάληψη θα θεωρείται επιτυχής εάν τουλάχιστον οι μισές από τις υποκατηγορίες επιτύχουν (μην κάνετε λάθος). Η γραμμή 6 δηλώνει a finally σύνδεσμος που θα κληθεί αν η παρακάτω αλυσίδα επιτυγχάνει ή αποτύχει. Στο finally σύνδεση, μεταβαίνουμε στο κύριο νήμα (γραμμή 7) και καλούμε singleUploadCompletion (γραμμή 8). Στη γραμμή 10, ορίσαμε έναν μέγιστο παραλληλισμό 1 (μεμονωμένη εκτέλεση) γύρω από τη λειτουργία εξαγωγής (γραμμή 11). Η γραμμή 13 μεταβαίνει στην ιδιωτική ουρά που ανήκει στο managedObjectContext παράδειγμα. Η γραμμή 14 δηλώνει μία προσπάθεια επανάληψης για τη λειτουργία μεταφόρτωσης (γραμμή 15). Η γραμμή 17 αλλάζει ξανά στο κύριο νήμα και 18 επικαλείται singleUploadSuccess. Μέχρι τη γραμμή 20 θα εκτελεστεί, όλες οι παράλληλες επαναλήψεις έχουν ολοκληρωθεί. Εάν αποτύχουν λιγότερες από τις μισές επαναλήψεις, τότε η γραμμή 20 μεταβαίνει στην κύρια ουρά για τελευταία φορά (θυμηθείτε ότι η καθεμία εκτελέστηκε στην ουρά παρασκηνίου), 21 πέσει η εισερχόμενη τιμή (ακόμα mediaReferences) και 22 επικαλείται totalProcessSuccess.

Η φόρμα HoneyBee είναι πιο καθαρή, πιο καθαρή και πιο ευανάγνωστη, για να μην αναφέρουμε ευκολότερα τη συντήρηση. Τι θα συνέβαινε στη μακροχρόνια μορφή αυτού του αλγορίθμου εάν απαιτείται ο βρόχος για την επανένταξη των αντικειμένων πολυμέσων σε έναν πίνακα όπως μια λειτουργία χάρτη; Αφού κάνατε την αλλαγή, πόσο σίγουροι θα ήσασταν ότι όλες οι απαιτήσεις του αλγορίθμου εξακολουθούσαν να πληρούνται; Στη φόρμα HoneyBee, αυτή η αλλαγή θα ήταν να αντικατασταθεί το καθένα με χάρτη για να χρησιμοποιηθεί μια παράλληλη λειτουργία χάρτη. (Ναι, έχει μειωθεί επίσης.)

Το HoneyBee είναι μια ισχυρή βιβλιοθήκη μελλοντικής εκπλήρωσης για το Swift που κάνει τη γραφή ασύγχρονοι και ταυτόχρονοι αλγόριθμοι ευκολότεροι, ασφαλέστεροι και πιο εκφραστικοί. Σε αυτό το άρθρο, έχουμε δει πώς η HoneyBee μπορεί να διευκολύνει τη συντήρηση των αλγορίθμων σας, να είναι πιο σωστή και πιο γρήγορη. Το HoneyBee έχει επίσης υποστήριξη για άλλα βασικά παραδείγματα ασύγχρονου, όπως δοκιμή επανάληψης, πολλαπλοί χειριστές σφαλμάτων, προστασία πόρων και επεξεργασία συλλογής (μορφές χάρτη, φίλτρο και μείωση ασύγχρονων). Για μια πλήρη λίστα χαρακτηριστικών ανατρέξτε στο δικτυακός τόπος . Για να μάθετε περισσότερα ή να κάνετε ερωτήσεις δείτε το ολοκαίνουργιο κοινοτικά φόρουμ .

Παράρτημα: Διασφάλιση της συμβατικής ορθότητας των λειτουργιών Async

Η διασφάλιση της συμβατικής ορθότητας των λειτουργιών αποτελεί θεμελιώδες δόγμα της επιστήμης των υπολογιστών. Τόσο πολύ που σχεδόν όλοι οι σύγχρονοι μεταγλωττιστές έχουν ελέγχους για να διασφαλίσουν ότι μια συνάρτηση που δηλώνει ότι επιστρέφει μια τιμή, επιστρέφει ακριβώς μία φορά. Η επιστροφή λιγότερη ή περισσότερες από μία φορές αντιμετωπίζεται ως σφάλμα και αποτρέπει κατάλληλα μια πλήρη συλλογή.

Αλλά αυτή η βοήθεια μεταγλωττιστή συνήθως δεν ισχύει για ασύγχρονες λειτουργίες. Εξετάστε το ακόλουθο (παιχνιδιάρικο) παράδειγμα:

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') } }

Το generateIcecream Η συνάρτηση δέχεται ένα Int και επιστρέφει ασύγχρονα ένα String. Ο γρήγορος μεταγλωττιστής αποδέχεται με χαρά την παραπάνω φόρμα ως σωστή, παρόλο που περιέχει ορισμένα προφανή προβλήματα. Δεδομένων ορισμένων εισόδων, αυτή η συνάρτηση μπορεί να καλέσει την ολοκλήρωση μηδέν, μία ή δύο φορές. Οι προγραμματιστές που έχουν εργαστεί με λειτουργίες async συχνά θα θυμούνται παραδείγματα αυτού του προβλήματος στη δουλειά τους. Τι μπορούμε να κάνουμε? Σίγουρα, θα μπορούσαμε να αναδιαμορφώσουμε τον κώδικα ώστε να είναι πιο καθαρός (ένας διακόπτης με θήκες εύρους θα λειτουργούσε εδώ). Αλλά μερικές φορές η λειτουργική πολυπλοκότητα είναι δύσκολο να μειωθεί. Δεν θα ήταν καλύτερο αν ο μεταγλωττιστής θα μπορούσε να μας βοηθήσει στην επαλήθευση της ορθότητας όπως συμβαίνει με τις τακτικές επιστροφές λειτουργιών;

Αποδεικνύεται ότι υπάρχει τρόπος. Παρατηρήστε την ακόλουθη μαγεία Swifty:

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') } }

Οι τέσσερις γραμμές που εισάγονται στο πάνω μέρος αυτής της συνάρτησης αναγκάζουν τον μεταγλωττιστή να επαληθεύσει ότι η επιστροφή κλήσης ολοκλήρωσης καλείται ακριβώς μία φορά, πράγμα που σημαίνει ότι αυτή η συνάρτηση δεν συντάσσεται πλέον. Τι συμβαίνει? Στην πρώτη γραμμή, δηλώνουμε αλλά δεν αρχικοποιούμε το αποτέλεσμα που τελικά θέλουμε να παράγει αυτή η συνάρτηση. Αφήνοντάς το απροσδιόριστο, διασφαλίζουμε ότι πρέπει να αντιστοιχιστεί μία φορά πριν μπορεί να χρησιμοποιηθεί, και δηλώνοντάς το ας διασφαλίσουμε ότι δεν μπορεί να αντιστοιχιστεί ποτέ σε δύο φορές. Η δεύτερη γραμμή είναι μια αναβολή που θα εκτελεστεί ως η τελική ενέργεια αυτής της λειτουργίας. Επικαλείται το μπλοκ ολοκλήρωσης με finalResult - αφού έχει ανατεθεί από την υπόλοιπη λειτουργία. Η γραμμή 3 δημιουργεί μια νέα σταθερά που ονομάζεται ολοκλήρωση που σκιάζει την παράμετρο επανάκλησης. Η νέα ολοκλήρωση είναι τύπου Void που δεν δηλώνει δημόσιο API. Αυτή η γραμμή διασφαλίζει ότι οποιαδήποτε χρήση ολοκλήρωσης μετά από αυτήν τη γραμμή θα είναι σφάλμα μεταγλωττιστή. Η αναβολή στη γραμμή 2 είναι η μόνη επιτρεπόμενη χρήση του μπλοκ ολοκλήρωσης. Η γραμμή 4 καταργεί μια προειδοποίηση μεταγλωττιστή που διαφορετικά θα υπήρχε σχετικά με τη χρήση της νέας σταθεράς ολοκλήρωσης.

Αναγκάσαμε λοιπόν με επιτυχία το πρόγραμμα γρήγορης μεταγλώττισης να αναφέρει ότι αυτή η ασύγχρονη λειτουργία δεν εκπληρώνει τη σύμβασή της. Ας ακολουθήσουμε τα βήματα για να το κάνουμε σωστό. Αρχικά, ας αντικαταστήσουμε όλη την άμεση πρόσβαση στην επιστροφή κλήσης με μια ανάθεση σε 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' } }

Τώρα ο μεταγλωττιστής αναφέρει δύο προβλήματα:

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'

Όπως αναμενόταν, η συνάρτηση έχει ένα μονοπάτι όπου 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' } else { finalResult = 'Neapolitan' } }

Το «Φιστίκι» έχει μετακινηθεί σε μια κατάλληλη ρήτρα και συνειδητοποιούμε ότι δεν καταφέραμε να καλύψουμε τη γενική υπόθεση - που φυσικά είναι «Ναπολιτάν».

Τα μοτίβα που μόλις περιγράφηκαν μπορούν εύκολα να προσαρμοστούν ώστε να επιστρέφουν προαιρετικές τιμές, προαιρετικά σφάλματα ή σύνθετους τύπους όπως το κοινό αποτέλεσμα. Αναγκάζοντας τον μεταγλωττιστή να επαληθεύσει ότι οι επιστροφές κλήσεων πραγματοποιούνται ακριβώς μία φορά, μπορούμε να επιβεβαιώσουμε την ορθότητα και την πληρότητα των ασύγχρονων συναρτήσεων.

βέλτιστες πρακτικές λογιστικού σχεδίου

Κατανόηση των βασικών

Τι είναι το ταυτόχρονο στον προγραμματισμό;

Ένας παράλληλος αλγόριθμος (που ονομάζεται επίσης παράλληλος προγραμματισμός) είναι ένας αλγόριθμος που έχει σχεδιαστεί για να εκτελεί ταυτόχρονα πολλαπλές (ίσως πολλές) λειτουργίες για να εκμεταλλευτεί περισσότερους πόρους υλικού και να μειώσει τον συνολικό χρόνο εκτέλεσης.

Ποια είναι τα προβλήματα της ταυτόχρονης;

Το σχήμα 'πυραμίδα της καταστροφής' των ένθετων μπλοκ κώδικα μπορεί γρήγορα να γίνει δυσκίνητο, καθώς ο ασύγχρονος χειρισμός σφαλμάτων μπορεί να είναι ακούσιος ή ελλιπής, ζητήματα με ενσωματώσεις τρίτων επιδεινώνονται και ενώ προορίζονται για κέρδη απόδοσης, μπορεί να οδηγήσουν σε σπατάλη και υπο-βέλτιστο εκτέλεση.