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

Разширено съчетание в Swift с HoneyBee



Проектиране, тестване и поддържане на едновременни алгоритми в Бързо е трудно и правилното получаване на подробностите е от решаващо значение за успеха на приложението ви. Едновременният алгоритъм (наричан още паралелно програмиране) е алгоритъм, който е проектиран да изпълнява едновременно множество (може би много) операции, за да се възползва от повече хардуерни ресурси и да намали общото време за изпълнение.

На платформите на Apple традиционният начин за писане на едновременни алгоритми е NSO операция . Дизайнът на NSOperation приканва програмиста да подраздели едновременния алгоритъм на отделни продължителни асинхронни задачи. Всяка задача ще бъде дефинирана в свой собствен подклас на NSOperation и екземплярите на тези класове ще бъдат комбинирани чрез обективен API, за да се създаде частичен ред от задачи по време на изпълнение. Този метод за проектиране на едновременни алгоритми беше най-модерното в платформите на Apple в продължение на седем години.



През 2014 г. Apple представи Grand Central Dispatch (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 лесно, изразително и безопасно. Нека препишем горния асинхронен алгоритъм с 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, който представлява медиен актив, качен на вашия сървър. На потребителя трябва да бъде позволено да избере десетки медийни елементи наведнъж и да ги качи на партида в бекенд системата. Медиите първо се представят чрез референтен низ, който трябва да се преобразува в медиен обект. За щастие приложението ви вече съдържа помощен метод, който прави точно това:

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 е по-ясна, по-чиста и по-лесна за четене, да не говорим за по-лесна за поддръжка. Какво би се случило с дългата форма на този алгоритъм, ако цикълът се изискваше за реинтегриране на Media обектите в масив като функция на картата? След като сте направили промяната, доколко ще бъдете уверени, че всички изисквания на алгоритъма все още са изпълнени? Във формата HoneyBee тази промяна ще бъде да замени всяка с карта, за да се използва паралелна функция на картата. (Да, и това е намалило.)

HoneyBee е мощна фючърсна библиотека за Swift, която прави писането на асинхронни и едновременни алгоритми по-лесно, по-безопасно и изразително. В тази статия видяхме как HoneyBee може да направи алгоритмите ви по-лесни за поддръжка, по-правилни и по-бързи. HoneyBee също има поддръжка за други ключови асинхронни парадигми като поддръжка за повторен опит, множество обработчици на грешки, защита на ресурси и обработка на колекции (асинхронни форми на карта, филтриране и намаляване). За пълен списък с функции вижте уебсайт . За да научите повече или да зададете въпроси, вижте чисто новото общностни форуми .

Приложение: Осигуряване на договорна коректност на асинхронните функции

Осигуряването на договорната коректност на функциите е основен принцип на компютърните науки. Дотолкова, че на практика всички съвременни компилатори имат проверки, за да гарантират, че функция, която декларира, че връща стойност, се връща точно веднъж. Връщането по-малко или повече от веднъж се третира като грешка и по подходящ начин предотвратява пълната компилация.

Но тази помощ за компилатора обикновено не се отнася за асинхронни функции. Помислете за следния (игрив) пример:

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. Бързият компилатор с радост приема горната форма като правилна, въпреки че съдържа някои очевидни проблеми. При определени входове тази функция може да извика завършване нула, един или два пъти. Програмистите, които са работили с асинхронни функции, често ще припомнят примери за този проблем в собствената си работа. Какво можем да направим? Разбира се, бихме могли да рефакторираме кода, за да бъде по-чист (тук ще работи превключвател с случаи на диапазон). Но понякога функционалната сложност е трудно да се намали. Не би ли било по-добре, ако компилаторът може да ни помогне при проверката на коректността, точно както прави с редовно връщащите се функции?

Оказва се, че има начин. Спазвайте следното 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, който представлява медиен актив, качен на вашия сървър. На потребителя трябва да бъде позволено да избере десетки медийни елементи наведнъж и да ги качи на партида в бекенд системата. Медиите първо се представят чрез референтен низ, който трябва да се преобразува в медиен обект. За щастие приложението ви вече съдържа помощен метод, който прави точно това:

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 е по-ясна, по-чиста и по-лесна за четене, да не говорим за по-лесна за поддръжка. Какво би се случило с дългата форма на този алгоритъм, ако цикълът се изискваше за реинтегриране на Media обектите в масив като функция на картата? След като сте направили промяната, доколко ще бъдете уверени, че всички изисквания на алгоритъма все още са изпълнени? Във формата HoneyBee тази промяна ще бъде да замени всяка с карта, за да се използва паралелна функция на картата. (Да, и това е намалило.)

HoneyBee е мощна фючърсна библиотека за Swift, която прави писането на асинхронни и едновременни алгоритми по-лесно, по-безопасно и изразително. В тази статия видяхме как HoneyBee може да направи алгоритмите ви по-лесни за поддръжка, по-правилни и по-бързи. HoneyBee също има поддръжка за други ключови асинхронни парадигми като поддръжка за повторен опит, множество обработчици на грешки, защита на ресурси и обработка на колекции (асинхронни форми на карта, филтриране и намаляване). За пълен списък с функции вижте уебсайт . За да научите повече или да зададете въпроси, вижте чисто новото общностни форуми .

Приложение: Осигуряване на договорна коректност на асинхронните функции

Осигуряването на договорната коректност на функциите е основен принцип на компютърните науки. Дотолкова, че на практика всички съвременни компилатори имат проверки, за да гарантират, че функция, която декларира, че връща стойност, се връща точно веднъж. Връщането по-малко или повече от веднъж се третира като грешка и по подходящ начин предотвратява пълната компилация.

Но тази помощ за компилатора обикновено не се отнася за асинхронни функции. Помислете за следния (игрив) пример:

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. Бързият компилатор с радост приема горната форма като правилна, въпреки че съдържа някои очевидни проблеми. При определени входове тази функция може да извика завършване нула, един или два пъти. Програмистите, които са работили с асинхронни функции, често ще припомнят примери за този проблем в собствената си работа. Какво можем да направим? Разбира се, бихме могли да рефакторираме кода, за да бъде по-чист (тук ще работи превключвател с случаи на диапазон). Но понякога функционалната сложност е трудно да се намали. Не би ли било по-добре, ако компилаторът може да ни помогне при проверката на коректността, точно както прави с редовно връщащите се функции?

Оказва се, че има начин. Спазвайте следното 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' } }

„Шам-фъстъкът“ е преместен в подходяща друга клауза и ние осъзнаваме, че не успяхме да обхванем общия случай - който разбира се е „неаполитански“.

Току-що описаните модели могат лесно да бъдат коригирани, за да върнат незадължителни стойности, незадължителни грешки или сложни типове като общото изброяване на резултатите. Принуждавайки компилатора да провери дали обратните извиквания се извикват точно веднъж, можем да потвърдим коректността и пълнотата на асинхронните функции.

Разбиране на основите

Какво е едновременност в програмирането?

Едновременният алгоритъм (наричан още паралелно програмиране) е алгоритъм, който е проектиран да изпълнява едновременно множество (може би много) операции, за да се възползва от повече хардуерни ресурси и да намали общото време за изпълнение.

Какви са проблемите с едновременността?

Формата на „пирамидата на обречеността“ на вложени кодови блокове може бързо да стане тромава, асинхронната обработка на грешки може да бъде неинтуитивна или непълна, проблемите с интеграциите на трети страни се изострят и макар да са предназначени за повишаване на производителността, това може да доведе до разточителство и неоптимално производителност.