Писане едновременни програми трудно е. Трябва да се справяте с нишки, брави, условия на състезание и т.н. е много податлив на грешки и може да доведе до код, който е труден за четене, тестване и поддръжка.
Затова мнозина предпочитат да избягват изцяло многопоточността. Вместо това те използват изключително еднонишкови процеси, разчитайки на външни услуги (като бази данни, опашки и т.н.), за да се справят с всички необходими едновременни или асинхронни операции. Въпреки че този подход в някои случаи е легитимна алтернатива, има много сценарии, при които той просто не е жизнеспособен вариант. Много системи в реално време - като приложения за търговия или банки или игри в реално време - нямат лукса да чакат да завърши процес с една нишка (те се нуждаят от отговора сега!). Другите системи са толкова изчислителни или ресурсно интензивни, че ще им отнеме необикновено много време (часове или дори дни в някои случаи), за да стартират, без да въвеждат паралелизация в кода си.
Един доста често срещан подход с една нишка (широко използван в Node.js света например) е да се използва базирана на събития неблокираща парадигма. Въпреки че това помага за производителността, като избягва превключванията на контекста, заключванията и блокирането, то все още не разглежда проблемите с едновременното използване на множество процесори (това би изисквало стартиране и координиране между множество независими процеси).
Така че това означава ли, че нямате друг избор, освен да пътувате дълбоко в недрата на нишките, бравите и условията на състезанието, за да създадете едновременно приложение?
Благодарение на рамката Akka, отговорът е отрицателен. Този урок представя примери на Akka и изследва начините, по които улеснява и опростява изпълнението на едновременни, разпределени приложения.
Акка е набор от инструменти и време за изпълнение за изграждане на силно едновременни, разпределени и устойчиви на грешки приложения на JVM. Akka е написан на Стълба , с езикови обвързвания, предвидени както за Scala, така и за Java.
Подходът на Akka за работа с едновременност се основава на Актьорски модел . В система, базирана на актьори, всичко е актьор, по същия начин, по който всичко е обект в обектно-ориентиран дизайн. Ключова разлика, макар и особено подходяща за нашата дискусия, е, че моделът на актьора е специално проектиран и проектиран да служи като едновременен модел, докато обектно-ориентираният модел не е такъв. По-конкретно, в система от актьори Scala, актьорите си взаимодействат и споделят информация, без никаква предпоставка за последователност. Механизмът, чрез който участниците споделят информация помежду си и си задават взаимно, е предаването на съобщения.
Цялата сложност на създаване и планиране на нишки, получаване и изпращане на съобщения и обработка на състезателни условия и синхронизация е прехвърлена в рамката, за да се обработва прозрачно.Akka създава слой между актьорите и основната система, така че актьорите просто трябва да обработват съобщения. Цялата сложност на създаване и планиране на нишки, получаване и изпращане на съобщения и обработка на състезателни условия и синхронизация е прехвърлена в рамката, за да се обработва прозрачно.
Akka стриктно се придържа към Реактивният манифест . Реактивните приложения имат за цел да заменят традиционните многонишкови приложения с архитектура, която отговаря на едно или повече от следните изисквания:
По същество актьорът не е нищо повече от обект, който получава съобщения и предприема действия, за да се справи с тях. Той е отделен от източника на съобщението и единствената му отговорност е да разпознава правилно типа съобщение, което е получил, и да предприеме съответните действия.
След получаване на съобщение, актьор може да предприеме едно или повече от следните действия:
Алтернативно, актьорът може да избере да игнорира съобщението изцяло (т.е. може да избере бездействие), ако сметне за подходящо да го направи.
За да се приложи актьор, е необходимо да се разшири чертата на akka.actor.Actor и да се приложи методът за получаване. Методът за получаване на актьор се извиква (от Akka), когато се изпрати съобщение до този актьор. Типичното му изпълнение се състои от съвпадение на шаблони, както е показано в следващия пример на Akka, за идентифициране на типа съобщение и реагиране по съответния начин:
import akka.actor.Actor import akka.actor.Props import akka.event.Logging class MyActor extends Actor { def receive = { case value: String => doSomething(value) case _ => println('received unknown message') } }
Съпоставянето на образци е относително елегантна техника за обработка на съобщения, която има тенденция да създава „по-чист“ и по-лесен за навигация код, отколкото сравнима реализация, базирана на обратно извикване. Помислете например за опростено изпълнение на HTTP заявка / отговор.
Първо, нека приложим това, като използваме парадигма, базирана на обратно повикване в JavaScript:
route(url, function(request){ var query = buildQuery(request); dbCall(query, function(dbResponse){ var wsRequest = buildWebServiceRequest(dbResponse); wsCall(wsRequest, function(wsResponse) { sendReply(wsResponse); }); }); });
Сега нека сравним това с изпълнение, основано на съвпадение на шаблони:
msg match { case HttpRequest(request) => { val query = buildQuery(request) dbCall(query) } case DbResponse(dbResponse) => { var wsRequest = buildWebServiceRequest(dbResponse); wsCall(dbResponse) } case WsResponse(wsResponse) => sendReply(wsResponse) }
Въпреки че JavaScript кодът, базиран на обратно повикване, е наистина компактен, със сигурност е по-трудно да се чете и навигира. За сравнение, базираният на съвпадение на шаблона код прави по-непосредствено ясно какви случаи се разглеждат и как се обработва всеки.
Приемането на сложен проблем и рекурсивното му разделяне на по-малки подпроблеми е добра техника за решаване на проблеми като цяло. Този подход може да бъде особено полезен в компютърните науки (в съответствие с Принцип на единна отговорност ), тъй като има тенденция да дава чист, модулиран код, с малко или никакво излишък, който е относително лесен за поддръжка.
В дизайн, базиран на актьор, използването на тази техника улеснява логическата организация на актьорите в йерархична структура, известна като Актьорска система . Актьорската система осигурява инфраструктурата, чрез която актьорите взаимодействат помежду си.
В Akka единственият начин за комуникация с актьор е чрез ActorRef
. An ActorRef
представлява препратка към актьор, който пречи на други обекти да имат пряк достъп или манипулират вътрешността и състоянието на този актьор. Съобщенията могат да бъдат изпращани до актьор чрез ActorRef
използвайки един от следните синтаксисни протоколи:
!
(“Кажи”) - изпраща съобщението и се връща незабавно?
(„Попитай“) - изпраща съобщението и връща a Бъдеще представляващ възможен отговорВсеки актьор има пощенска кутия, до която се доставят входящите му съобщения. Има няколко реализации на пощенска кутия, от които да избирате, като изпълнението по подразбиране е FIFO.
Актьорът съдържа много променливи на екземпляра, за да поддържа състояние, докато обработва множество съобщения. Akka гарантира, че всеки екземпляр на актьор работи в собствена лека нишка и че съобщенията се обработват едно по едно. По този начин състоянието на всеки актьор може да бъде надеждно поддържано, без разработчикът да има нужда изрично да се тревожи за синхронизация или състезателни условия.
На всеки актьор се предоставя следната полезна информация за изпълнение на задачите му чрез API на Akka Actor:
sender
: an ActorRef
на подателя на съобщението, което се обработва в моментаcontext
: информация и методи, свързани с контекста, в който се изпълнява актьорът (включва например actorOf
метод за създаване на инстанция на нов актьор)supervisionStrategy
: определя стратегията, която да се използва за възстановяване от грешкиself
: ActorRef
за самия актьорЗа да помогнем да свържем тези уроци заедно, нека разгледаме прост пример за преброяване на броя думи в текстов файл.
За целите на нашия пример за Akka ще разложим проблема на две подзадачи; а именно, (1) „дъщерна“ задача за преброяване на броя на думите в един ред и (2) „родителска“ задача за сумиране на броя на думите на всеки ред, за да се получи общият брой думи във файла.
Родителският актьор ще зареди всеки ред от файла и след това ще делегира на дъщерен актьор задачата да преброи думите в този ред. Когато детето приключи, то ще изпрати съобщение до родителя с резултата. Родителят ще получи съобщенията с броя на думите (за всеки ред) и ще запази брояч за общия брой думи в целия файл, който след това ще върне на своя извикващ при завършване.
(Обърнете внимание, че предоставените по-долу образци на учебни кодове на Akka са предназначени да бъдат само дидактични и следователно не се отнасят непременно към всички крайни условия, оптимизации на производителността и т.н. това същност .)
Нека първо разгледаме примерна реализация на детето StringCounterActor
клас:
case class ProcessStringMsg(string: String) case class StringProcessedMsg(words: Integer) class StringCounterActor extends Actor { def receive = { case ProcessStringMsg(string) => { val wordsInLine = string.split(' ').length sender ! StringProcessedMsg(wordsInLine) } case _ => println('Error: message not recognized') } }
Този актьор има много проста задача: консумира ProcessStringMsg
съобщения (съдържащи ред текст), пребройте броя на думите на посочения ред и върнете резултата на подателя чрез StringProcessedMsg
съобщение. Имайте предвид, че сме внедрили нашия клас, за да използваме !
(“Кажи”) метод за изпращане на StringProcessedMsg
съобщение (т.е. да изпратите съобщението и да се върнете незабавно).
Добре, сега нека насочим вниманието си към родителя WordCounterActor
клас:
1. case class StartProcessFileMsg() 2. 3. class WordCounterActor(filename: String) extends Actor { 4. 5. private var running = false 6. private var totalLines = 0 7. private var linesProcessed = 0 8. private var result = 0 9. private var fileSender: Option[ActorRef] = None 10. 11. def receive = { 12. case StartProcessFileMsg() => { 13. if (running) { 14. // println just used for example purposes; 15. // Akka logger should be used instead 16. println('Warning: duplicate start message received') 17. } else { 18. running = true 19. fileSender = Some(sender) // save reference to process invoker 20. import scala.io.Source._ 21. fromFile(filename).getLines.foreach { line => 22. context.actorOf(Props[StringCounterActor]) ! ProcessStringMsg(line) 23. totalLines += 1 24. } 25. } 26. } 27. case StringProcessedMsg(words) => { 28. result += words 29. linesProcessed += 1 30. if (linesProcessed == totalLines) { 31. fileSender.map(_ ! result) // provide result to process invoker 32. } 33. } 34. case _ => println('message not recognized!') 35. } 36. }
Тук се случват много неща, така че нека разгледаме всяко от тях по-подробно (имайте предвид, че номерата на редовете, посочени в дискусията, която следва, се основават на горния пример на код) ...
Първо, обърнете внимание, че името на файла за обработка се предава на WordCounterActor
конструктор (ред 3). Това показва, че актьорът трябва да се използва само за обработка на един файл. Това също опростява заданието за кодиране за разработчика, като се избягва необходимостта от нулиране на променливите на състоянието (running
, totalLines
, linesProcessed
и result
), когато работата е завършена, тъй като екземплярът се използва само веднъж (т.е. за обработка на един файл) и след това се изхвърля.
След това забележете, че WordCounterActor
обработва два вида съобщения:
StartProcessFileMsg
(ред 12)WordCounterActor
.WordCounterActor
първо проверява дали не получава излишна заявка.WordCounterActor
генерира предупреждение и нищо повече не се прави (ред 16).WordCounterActor
съхранява препратка към подателя в fileSender
инстанция променлива (имайте предвид, че това е Option[ActorRef]
, а не Option[Actor]
- вижте ред 9). Това ActorRef
е необходим, за да има достъп по-късно и да отговори на него при обработката на окончателния StringProcessedMsg
(което се получава от StringCounterActor
дете, както е описано по-долу).WordCounterActor
след това чете файла и, когато всеки ред във файла се зарежда, StringCounterActor
дете се създава и към него се предава съобщение, съдържащо реда, който трябва да бъде обработен (редове 21-24).StringProcessedMsg
(ред 27)StringCounterActor
когато завърши обработката на присвоената му линия.WordCounterActor
увеличава брояча на редове за файла и ако всички редове във файла са обработени (т.е. когато totalLines
и linesProcessed
са равни), той изпраща крайния резултат към оригинала fileSender
(редове 28-31).За пореден път забележете, че в Akka единственият механизъм за комуникация между участниците е предаването на съобщения. Съобщенията са единственото нещо, което актьорите споделят и тъй като актьорите могат да имат достъп едновременно до едни и същи съобщения, за тях е важно да бъдат неизменни, за да се избегнат расовите условия и неочакваното поведение.
Класове по дела в Scala са редовни класове, които осигуряват рекурсивен механизъм на разлагане чрез съвпадение на шаблони.Поради това е обичайно да се предават съобщения под формата на класове случаи, тъй като те са неизменяеми по подразбиране и поради това колко лесно се интегрират със съвпадение на шаблони.
Нека завършим примера с пример за код, за да стартираме цялото приложение.
object Sample extends App { import akka.util.Timeout import scala.concurrent.duration._ import akka.pattern.ask import akka.dispatch.ExecutionContexts._ implicit val ec = global override def main(args: Array[String]) { val system = ActorSystem('System') val actor = system.actorOf(Props(new WordCounterActor(args(0)))) implicit val timeout = Timeout(25 seconds) val future = actor ? StartProcessFileMsg() future.map { result => println('Total number of words ' + result) system.shutdown } } }
При едновременното програмиране „бъдещето“ е по същество обект на резервоар за резултат, който все още не е известен.Забележете как този път ?
метод се използва за изпращане на съобщение. По този начин повикващият може да използва върнатото Бъдеще за отпечатване на крайния резултат, когато това е налично и за излизане от програмата чрез изключване на ActorSystem.
В една актьорска система всеки актьор е надзорник на своите деца. Ако даден актьор не успее да обработи съобщение, той спира себе си и всички свои деца и изпраща съобщение, обикновено под формата на изключение, до своя ръководител.
В Akka стратегиите за надзор са основният и ясен механизъм за определяне на устойчивото на повреди поведение на вашата система.В Akka начинът, по който супервайзорът реагира и се справя с изключенията, произтичащи от неговите деца, се нарича стратегия за супервизор. Стратегии за супервизор са основният и ясен механизъм, чрез който определяте поведението на вашата система, устойчиво на грешки.
Когато съобщение, означаващо неизправност, достигне до надзорник, то може да предприеме едно от следните действия:
Освен това, Актьор може да реши да приложи действието само към провалилите се деца или към своите братя и сестри. Има две предварително определени стратегии за това:
OneForOneStrategy
: Прилага посоченото действие само към неуспешно детеAllForOneStrategy
: Прилага посоченото действие към всички негови децаЕто един прост пример, използвайки OneForOneStrategy
:
import akka.actor.OneForOneStrategy import akka.actor.SupervisorStrategy._ import scala.concurrent.duration._ override val supervisorStrategy = OneForOneStrategy() { case _: ArithmeticException => Resume case _: NullPointerException => Restart case _: IllegalArgumentException => Stop case _: Exception => Escalate }
Ако не е посочена стратегия, се използва следната стратегия по подразбиране:
Изпълнението на Akka на тази стратегия по подразбиране е както следва:
final val defaultStrategy: SupervisorStrategy = { def defaultDecider: Decider = { case _: ActorInitializationException ⇒ Stop case _: ActorKilledException ⇒ Stop case _: Exception ⇒ Restart } OneForOneStrategy()(defaultDecider) }
Akka позволява изпълнението на персонализирани надзорни стратегии , но както предупреждава документацията на Akka, правете това с повишено внимание, тъй като неправилните имплементации могат да доведат до проблеми като блокирани системи от актьори (т.е. окончателно спрени актьори).
Архитектурата на Akka поддържа прозрачност на местоположението , позволявайки на актьорите да бъдат изцяло агностични към мястото, от което произхождат съобщенията, които получават. Подателят на съобщението може да се намира в същия JVM като актьора или в отделен JVM (или изпълняващ се на същия възел или различен възел). Akka дава възможност всеки от тези случаи да бъде обработен по начин, който е напълно прозрачен за актьора (и следователно за разработчика). Единственото предупреждение е, че съобщенията, изпратени през множество възли, трябва да могат да се сериализират.
Архитектурата Akka поддържа прозрачност на местоположението, позволявайки на актьорите да бъдат изцяло агностични към мястото, откъдето произхождат съобщенията, които получават.Системите на Actor са проектирани да работят в разпределена среда, без да се изисква какъвто и да е специализиран код. Akka изисква само наличието на конфигурационен файл (application.conf
), който определя възлите, на които да се изпращат съобщения. Ето един прост пример за конфигурационен файл:
akka { actor { provider = 'akka.remote.RemoteActorRefProvider' } remote { transport = 'akka.remote.netty.NettyRemoteTransport' netty { hostname = '127.0.0.1' port = 2552 } } }
Видяхме как рамката на Akka помага за постигане на едновременност и висока производителност. Както обаче посочих този урок, има няколко точки, които трябва да имате предвид при проектирането и внедряването на вашата система, за да се възползвате максимално от силата на Akka:
Актьорите трябва да обработват събития (т.е. да обработват съобщения) асинхронно и не трябва да блокират, в противен случай ще се случат превключвания на контекста, които могат да повлияят неблагоприятно на производителността. По-точно, най-добре е да извършвате блокиращи операции (IO и др.) В бъдеще, за да не блокирате актьора; т.е.:
case evt => blockingCall() // BAD case evt => Future { blockingCall() // GOOD }
Akka, написано на Стълба , опростява и улеснява разработването на силно едновременни, разпределени и устойчиви на грешки приложения, скривайки голяма част от сложността от разработчика. Изпълнението на Akka с пълна справедливост ще изисква много повече от този единствен урок, но се надяваме, че това въведение и неговите примери са достатъчно пленителни, за да ви накарат да искате да прочетете повече.
Amazon, VMWare и CSC са само няколко примера за водещи компании, които активно използват Akka. Посетете официален уебсайт Akka за да научите повече и да проучите дали Akka може да бъде правилният отговор и за вашия проект.