Pisanje sočasni programi je težko. Obravnavanje niti, ključavnic, dirkalnih pogojev itd. Je zelo nagnjeno k napakam in lahko vodi do kode, ki jo je težko prebrati, preizkusiti in vzdrževati.
Mnogi se zato raje izogibajo večnitnosti. Namesto tega uporabljajo izključno enonitne procese, pri čemer se zanašajo na zunanje storitve (kot so zbirke podatkov, čakalne vrste itd.) Za obdelavo vseh potrebnih sočasnih ali asinhronih operacij. Čeprav je ta pristop v nekaterih primerih legitimna alternativa, obstaja veliko scenarijev, v katerih preprosto ni izvedljiva možnost. Številni sistemi v realnem času - na primer trgovalne ali bančne aplikacije ali igre v realnem času - nimajo razkošja, da bi čakali, da se postopek z enim navojem zaključi (odgovor potrebujejo zdaj!). Drugi sistemi so tako zahtevni za računalnike ali vire, da bi jim trajal neomejen čas (v nekaterih primerih ure ali celo dnevi), ne da bi v njihovo kodo uvedli paralelizacijo.
Eden dokaj pogostih enonitnih pristopov (pogosto uporabljen v Node.js svetu) na primer uporabiti paradigmo, ki ne blokira dogodkov. Čeprav to pomaga pri zmogljivosti z izogibanjem stikalom, zaklepanjem in blokiranjem konteksta, še vedno ne obravnava težav pri sočasni uporabi več procesorjev (če bi to zahtevalo zagon in usklajevanje več neodvisnih procesov).
c corporation vs s corporation na w9
Torej to pomeni, da vam ni preostalo drugega, kot da potujete globoko v drobovje niti, ključavnic in dirkalnih pogojev, da ustvarite sočasno aplikacijo?
Zahvaljujoč ogrodju Akka je odgovor ne. Ta vadnica predstavlja primere Akka in raziskuje načine, kako olajšuje in poenostavlja izvajanje sočasnih porazdeljenih aplikacij.
Akka je komplet orodij in izvajalno okolje za izdelavo zelo sočasnih, porazdeljenih in odpornih aplikacij na JVM. Akka je napisan v jeziku Lestev , z jezikovnimi vezmi, zagotovljenimi tako za Scalo kot za Javo.
Akkin pristop k sočasni obravnavi temelji na Igralski model . V sistemu, ki temelji na igralcu, je vse igralec, podobno kot je vse predmet v objektno usmerjenem oblikovanju. Ključna razlika, ki je še posebej pomembna za našo razpravo, je, da je bil igralski model posebej zasnovan in oblikovan tako, da služi kot sočasni model, medtem ko objektno usmerjeni model ni. Natančneje, v sistemu igralcev Scala igralci sodelujejo in si izmenjujejo informacije brez kakršne koli predpostavke zaporednosti. Mehanizem, s katerim si akterji izmenjujejo informacije in si nalagajo drug drugega, je posredovanje sporočil.
Vsa zapletenost ustvarjanja in razporejanja niti, sprejemanja in odpošiljanja sporočil ter ravnanja z dirkalnimi pogoji in sinhronizacijo je za pregledno obdelavo prenesena v ogrodje.Akka ustvarja plast med igralci in osnovnim sistemom, tako da morajo igralci preprosto obdelovati sporočila. Vsa zapletenost ustvarjanja in razporejanja niti, sprejemanja in odpošiljanja sporočil ter ravnanja z dirkalnimi pogoji in sinhronizacijo je za pregledno obdelavo prenesena v ogrodje.
Akka se dosledno drži Reaktivni manifest . Cilj reaktivnih aplikacij je nadomestiti tradicionalne večnitne programe z arhitekturo, ki izpolnjuje eno ali več naslednjih zahtev:
Igralec v bistvu ni nič drugega kot objekt, ki sprejema sporočila in ukrepa z njimi. Ločeno je od vira sporočila in njegova edina odgovornost je pravilno prepoznati vrsto sporočila, ki ga je prejelo, in ustrezno ukrepati.
Po prejemu sporočila lahko igralec izvede eno ali več od naslednjih dejanj:
Igralec se lahko odloči, da bo sporočilo popolnoma prezrl (tj. Lahko izbere nedejavnost), če se mu to zdi primerno.
Za izvedbo igralca je treba razširiti lastnost akka.actor.Actor in uporabiti metodo sprejemanja. Način sprejema igralca se prikliče (Akka), ko se temu igralcu pošlje sporočilo. Njegova tipična izvedba je sestavljena iz ujemanja vzorcev, kot je prikazano v naslednjem primeru Akka, za prepoznavanje vrste sporočila in ustrezno odzivanje:
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') } }
Ujemanje vzorcev je sorazmerno elegantna tehnika za obdelavo sporočil, ki ponavadi ustvari 'čistejšo' in enostavnejšo navigacijo po kodi kot primerljiva izvedba, ki temelji na povratnih klicih. Razmislite na primer o poenostavljeni izvedbi zahteve / odgovora HTTP.
Najprej izvedimo to z uporabo paradigme, ki temelji na povratnem klicu v JavaScript:
route(url, function(request){ var query = buildQuery(request); dbCall(query, function(dbResponse){ var wsRequest = buildWebServiceRequest(dbResponse); wsCall(wsRequest, function(wsResponse) { sendReply(wsResponse); }); }); });
Zdaj pa primerjajmo to z izvedbo, ki temelji na ujemanju vzorcev:
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) }
Čeprav je koda JavaScript, ki temelji na povratnem klicu, sicer kompaktna, jo je vsekakor težje brati in krmariti. V primerjavi s kodo, ki temelji na ujemanju vzorcev, je takoj videti, kateri primeri se obravnavajo in kako se z njimi ravna.
Če zapletemo problem in ga rekurzivno razdelimo na manjše podprobleme, je na splošno dobra tehnika reševanja problemov. Ta pristop je lahko še posebej koristen v računalništvu (v skladu z Načelo enotne odgovornosti ), saj ponavadi daje čisto modularizirano kodo z malo ali nič odvečnosti, ki jo je relativno enostavno vzdrževati.
Pri zasnovi, ki temelji na igralcu, uporaba te tehnike olajša logično organizacijo igralcev v hierarhično strukturo, znano kot Igralski sistem . Sistem igralcev zagotavlja infrastrukturo, prek katere igralci medsebojno komunicirajo.
V Akki je edini način za komunikacijo z igralcem prek ActorRef
. An ActorRef
predstavlja sklic na igralca, ki drugim predmetom preprečuje neposreden dostop do notranjosti in stanja tega igralca ali njegovo manipulacijo. Sporočila lahko igralcu pošljete prek ActorRef
z uporabo enega od naslednjih sintaksnih protokolov:
!
(“Povej”) - pošlje sporočilo in se takoj vrne?
(»Vprašaj«) - pošlje sporočilo in vrne a Prihodnost predstavlja možen odgovorVsak igralec ima nabiralnik, v katerega so dostavljena njegova dohodna sporočila. Izbirate lahko med več izvedbami nabiralnikov, privzeta izvedba pa je FIFO.
Igralec vsebuje veliko spremenljivk primerka za vzdrževanje stanja med obdelavo več sporočil. Akka zagotavlja, da vsak primerek igralca deluje v svoji lahki niti in da se sporočila obdelujejo eno za drugo. Na ta način je mogoče zanesljivo vzdrževati stanje vsakega igralca, ne da bi se moral razvijalec izrecno skrbeti za sinhronizacijo ali dirkalne pogoje.
Vsakemu igralcu so prek Akka Actor API na voljo naslednje koristne informacije za izvajanje njegovih nalog:
sender
: an ActorRef
pošiljatelju sporočila, ki se trenutno obdelujecontext
: informacije in metode, ki se nanašajo na kontekst, v katerem deluje izvajalec (vključuje na primer actorOf
metodo za ustvarjanje novega akterja)supervisionStrategy
: določa strategijo, ki se uporablja za obnovo po napakahself
: ActorRef
za samega igralcaZa lažje povezovanje teh vadnic si oglejmo preprost primer štetja števila besed v besedilni datoteki.
Za namene našega primera Akka bomo problem razložili na dve podopravili; in sicer (1) naloga otroka za štetje števila besed v eni vrstici in (2) naloga nadrejenega seštevanja števila besed v vrstici, da dobimo skupno število besed v datoteki.
Nadrejeni igralec bo naložil vsako vrstico iz datoteke in nato nadrejenemu igralcu dodal nalogo štetja besed v tej vrstici. Ko je otrok končan, bo staršu poslal sporočilo z rezultatom. Nadrejeni bo prejel sporočila s številom besed (za vsako vrstico) in obdržal števec celotnega števila besed v celotni datoteki, ki ga bo po zaključku vrnil klicatelju.
(Upoštevajte, da so spodaj navedeni vzorci učnih kod Akka namenjeni zgolj didaktični in se zato ne nanašajo nujno na vse robne pogoje, optimizacije zmogljivosti itd. V nadaljevanju je na voljo tudi celotna združljiva različica vzorčnih kod, prikazana spodaj to bistvo .)
Oglejmo si najprej vzorčno izvedbo otroka StringCounterActor
razred:
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') } }
Ta igralec ima zelo preprosto nalogo: porabi ProcessStringMsg
sporočila (ki vsebujejo vrstico besedila), preštejte število besed v določeni vrstici in vrnite rezultat pošiljatelju prek StringProcessedMsg
sporočilo. Upoštevajte, da smo svoj razred uvedli tako, da uporablja !
(»Povej«) za pošiljanje StringProcessedMsg
sporočilo (tj. poslati sporočilo in se takoj vrniti).
V redu, zdaj se osredotočimo na starša WordCounterActor
razred:
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. }
Tu se dogaja veliko stvari, zato jih podrobneje preučimo (upoštevajte, da številke vrstic, navedene v razpravi, ki sledi, temeljijo na zgornjem vzorcu kode) ...
Najprej opazite, da se ime datoteke, ki jo želite obdelati, posreduje na WordCounterActor
konstruktor (vrstica 3). To pomeni, da se igralec uporablja samo za obdelavo ene datoteke. To razvijalcu tudi poenostavi nalogo kodiranja, tako da se izogne potrebi po ponastavitvi spremenljivk stanja (running
, totalLines
, linesProcessed
in result
), ko je opravilo končano, ker se primerek uporablja samo enkrat (tj. za obdelavo ene datoteke) in nato zavrže.
Nato opazite, da WordCounterActor
obravnava dve vrsti sporočil:
naštej in razloži tri pravila zaznavne organizacije
StartProcessFileMsg
(vrstica 12)WordCounterActor
.WordCounterActor
najprej preveri, ali ne prejema odvečne zahteve.WordCounterActor
ustvari opozorilo in nič več se ne naredi (vrstica 16).WordCounterActor
sklic na pošiljatelja shrani v fileSender
spremenljivka primerka (upoštevajte, da je to Option[ActorRef]
in ne Option[Actor]
- glejte vrstico 9). To ActorRef
je potreben za kasnejši dostop do njega in odziv nanj pri obdelavi končnega StringProcessedMsg
(ki ga prejme otrok StringCounterActor
, kot je opisano spodaj).WordCounterActor
nato prebere datoteko in, ko je vsaka vrstica v datoteki naložena, StringCounterActor
otrok se ustvari in mu se posreduje sporočilo, ki vsebuje vrstico, ki jo je treba obdelati (vrstice 21-24).StringProcessedMsg
(vrstica 27)StringCounterActor
ko zaključi z obdelavo vrstice, ki mu je dodeljena.WordCounterActor
poveča števec vrstic za datoteko in, če so vse vrstice v datoteki obdelane (tj. ko so totalLines
in linesProcessed
enake), pošlje končni rezultat izvirniku fileSender
(vrstice 28-31).Še enkrat opazimo, da je v Akki edini mehanizem za komunikacijo med akterji prenašanje sporočil. Sporočila so edina stvar, ki si jo delijo igralci, in ker lahko igralci lahko istočasno dostopajo do istih sporočil, je pomembno, da so nespremenljivi, da bi se izognili dirkalnim pogojem in nepričakovanemu vedenju.
Razredi primerov v Scali so redni razredi, ki z ujemanjem vzorcev zagotavljajo mehanizem rekurzivne razgradnje.Zato je običajno, da sporočila posredujemo v obliki razredov primerov, saj so privzeto nespremenljiva in zaradi tega, kako neopazno se integrirajo z ujemanjem vzorcev.
Zaključimo primer z vzorcem kode za zagon celotne aplikacije.
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 } } }
Pri sočasnem programiranju je 'prihodnost' v bistvu nadomestni objekt za rezultat, ki še ni znan.Opazite, kako je tokrat ?
za pošiljanje sporočila. Na ta način lahko klicatelj uporabi vrnjeno Prihodnost za tiskanje končnega rezultata, ko je ta na voljo, in za izhod iz programa z izklopom sistema ActorSystem.
V igralčevem sistemu je vsak igralec nadzornik svojih otrok. Če igralec ne obdeluje sporočila, začasno ustavi sebe in vse svoje otroke in nadzorniku pošlje sporočilo, običajno v obliki izjeme.
V programu Akka so nadzorniške strategije primarni in enostaven mehanizem za določanje odpornega vedenja vašega sistema.V Akki je način, na katerega se nadzornik odzove in obravnava izjeme, ki jih prenašajo njegovi otroci, omenjen kot strategija nadzornika. Supervizijske strategije so primarni in enostaven mehanizem, s katerim določite obnašanje vašega sistema, odpornega proti napakam.
Ko sporočilo, ki pomeni napako, prispe do nadzornika, lahko izvede eno od naslednjih dejanj:
Poleg tega se igralec lahko odloči, da bo to akcijo uporabil samo za propadle otroke ali za svoje brate in sestre. Za to obstajata dve vnaprej določeni strategiji:
OneForOneStrategy
: Uporabi določeno dejanje samo za neuspešnega otrokaAllForOneStrategy
: Uporabi določeno dejanje za vse njegove podrejene elementeTu je preprost primer z uporabo 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 }
Če nobena strategija ni določena, se uporabi naslednja privzeta strategija:
Izvajanje te privzete strategije, ki jo dobavlja Akka, je naslednje:
final val defaultStrategy: SupervisorStrategy = { def defaultDecider: Decider = { case _: ActorInitializationException ⇒ Stop case _: ActorKilledException ⇒ Stop case _: Exception ⇒ Restart } OneForOneStrategy()(defaultDecider) }
Akka omogoča izvajanje nadzorniške strategije po meri , vendar, kot opozarja dokumentacija Akka, to storite previdno, saj lahko nepravilne izvedbe povzročijo težave, kot so blokirani sistemi igralcev (tj. trajno izključeni igralci).
Arhitektura Akka podpira preglednost lokacije , ki igralcem omogoča, da so popolnoma agnostični, od kod izvirajo sporočila, ki jih prejmejo. Pošiljatelj sporočila lahko prebiva v istem JVM kot igralec ali v ločenem JVM (bodisi da se izvaja na istem vozlišču ali drugem vozlišču). Akka omogoča, da se vsak od teh primerov obravnava na način, ki je igralcu (in torej tudi razvijalcu) popolnoma pregleden. Edino opozorilo je, da je treba sporočila, poslana prek več vozlišč, serializirati.
Arhitektura Akka podpira preglednost lokacij, kar omogoča igralcem, da so popolnoma agnostični, od kod izvirajo sporočila, ki jih prejmejo.Sistemi igralcev so zasnovani tako, da delujejo v porazdeljenem okolju, ne da bi za to potrebovali posebno kodo. Akka zahteva le prisotnost konfiguracijske datoteke (application.conf
), ki določa vozlišča, ki jim želite pošiljati sporočila. Tu je preprost primer konfiguracijske datoteke:
kako pišeš kodo
akka { actor { provider = 'akka.remote.RemoteActorRefProvider' } remote { transport = 'akka.remote.netty.NettyRemoteTransport' netty { hostname = '127.0.0.1' port = 2552 } } }
Videli smo, kako ogrodje Akka pomaga doseči sočasnost in visoko zmogljivost. Vendar, kot je poudarjeno v tej vadnici, morate pri načrtovanju in izvajanju sistema upoštevati nekaj točk, da boste v celoti izkoristili moč Akke:
Igralci bi morali dogodke (tj. Obdelovati sporočila) obravnavati asinhrono in jih ne bi smeli blokirati, sicer se bodo zgodila stikala konteksta, ki lahko negativno vplivajo na uspešnost. Natančneje, najbolje je izvajati blokirne operacije (IO itd.) V prihodnosti, da ne bi blokirali igralca; tj .:
case evt => blockingCall() // BAD case evt => Future { blockingCall() // GOOD }
Akka, napisano v Lestev , poenostavlja in olajša razvoj zelo hkratnih, porazdeljenih in odpornih aplikacij, pri čemer veliko zahtevnosti skriva pred razvijalcem. Za popolno pravičnost Akke bi bilo treba veliko več kot le ta vadnica, vendar upamo, da so bili ta uvod in njegovi primeri dovolj privlačni, da ste želeli prebrati več.
Amazon, VMWare in CSC je le nekaj primerov vodilnih podjetij, ki aktivno uporabljajo Akko. Obiščite uradno spletno mesto Akka če želite izvedeti več in raziskati, ali je Akka lahko pravi odgovor tudi za vaš projekt.