de Martin Budi

Dacă utilizați în continuare Sincronizat, ar trebui să încercați în schimb Akka Actor – iată de ce

Daca utilizati in continuare Sincronizat ar trebui sa incercati in

Sincronizat este mecanismul tradițional de concurență Java. Deși probabil nu este ceva pe care îl vedem adesea în aceste zile, încă alimentează multe biblioteci. Problema este că sincronizarea este atât blocantă, cât și complicată. În acest articol, aș dori să ilustrez problema într-un mod simplu și raționamentul meu de a trece la Akka Actor pentru o concurență mai bună și mai ușoară.

Luați în considerare acest cod simplu:

  int x; 
 
 if (x > 0) {
   return true;
 } else {
   return false;
 }

Deci, reveniți adevărat dacă X este pozitiv. Simplu.

Apoi, luați în considerare acest cod și mai simplu:

x++;

Da, un ghișeu. Foarte simplu nu?

ad-banner

Cu toate acestea, toate aceste coduri pot exploda spectaculos într-un mediu cu mai multe fire.

În primul exemplu, adevărat sau fals nu este determinat de valoarea lui x. Este de fapt determinat de testul if. Deci, dacă un alt fir schimbă x într-un punct negativ imediat după ce primul fir a trecut testul if, am fi totuși adevărat chiar dacă x nu mai este pozitiv.

Al doilea exemplu este destul de înșelător. Deși este doar o singură linie, există de fapt trei operații: citirea X, incrementând-o și readucând valoarea actualizată. Dacă două fire rulează exact în același timp, actualizarea s-ar putea pierde.

Când avem diferite fire care accesează și modifică simultan o variabilă, avem o condiție de cursă. Dacă vrem doar să construim un contor, Java oferă variabile atomice sigure pentru fire, printre acestea număr întreg Atomic pe care îl putem folosi în acest scop. Totuși, Atomic Integer nu funcționează decât pe variabile unice. Cum facem mai multe operații atomice?

Prin utilizarea sincronizat bloc. În primul rând, să aruncăm o privire la un exemplu mai elaborat.

int x; 
public int withdraw(int deduct){
    int balance = x - deduct; 
    if (balance > 0) {
      x = balance;
      return deduct;
    } else {
      return 0;
    }
}

Aceasta este o metodă de bază pentru extragerea numerarului. Se întâmplă, de asemenea, să fie periculos. Două fire care rulează în același timp pot determina banca să emită două retrageri chiar dacă soldul nu mai este suficient. Acum să vedem cum funcționează cu blocul sincronizat:

volatile int x;
public int withdraw(int deduct){
  synchronized(this){
    int balance = x - deduct; 
    if (balance > 0) {
      x = balance;
      return deduct;
    } else {
      return 0;
    }
  }
}

Ideea blocului sincronizat este simplă. Un fir intră în el și îl blochează, în timp ce alte fire așteaptă afară. Încuietoarea este un obiect, în cazul nostru acest. După ce a terminat, blocarea este eliberată și trecută la un alt fir care apoi face același lucru. De asemenea, rețineți cuvântul cheie ezoteric volatil care este necesar pentru a împiedica firul să utilizeze memoria cache locală a procesorului X.

Acum, cu firele desfăcute, banca nu va emite accidental retrageri nefinanțate. Cu toate acestea, această structură tinde să crească complexă cu mai multe blocuri și mai multe încuietori. A face față mai multor încuietori este deosebit de riscant. Blocurile ar putea, din greșeală, să dețină cheia unul pentru celălalt și să ajungă să blocheze întreaga aplicație. Și pe deasupra, avem o problemă de eficiență. Amintiți-vă că, în timp ce un fir funcționează în interior, toate celelalte fire așteaptă afară. Și firele de așteptare sunt bine … așteaptă. Nu fac altceva decât să aștepte.

Deci, în loc să faceți un astfel de mecanism, de ce să nu renunțați doar la o coadă? Pentru a-l vizualiza mai bine, imaginați-vă un sistem de e-mail. Când trimiteți un e-mail, îl lăsați în căsuța poștală a destinatarului. Nu aștepți până când persoana o citește.

Acestea sunt elementele de bază ale modelului Actor și ale cadrului Akka în general.

Actorul încapsulează starea și comportamentul. Spre deosebire de încapsularea OOP, totuși, actorii nu își expun deloc starea și comportamentul. Singura modalitate prin care un actor poate comunica între ei este schimbul de mesaje. Mesajele primite sunt aruncate într-o cutie poștală și digerate în ordinea primului intrare în primul ieșire. Iată un eșantion refăcut în Akka și Scala.

case class Withdraw(deduct: Int)
class ReplicaActor extends Actor {
  var x = 10;
  def receive: Receive = {
    case Withdraw(deduct) => val r = withdraw(deduct)
  }
}
class BossActor extends Actor {
  var replica = context.actorOf(Props[ReplicaActor])
  replica ! Withdraw(6)
  replica ! Withdraw(9)   
}

Avem un ReplicaActor care face treaba și BossActor care comandă replica în jur. În primul rând, observați! semn sau spune. Aceasta este una dintre cele două metode (cealaltă este cere) pentru ca un actor să trimită un mesaj asincron unui alt actor. spune în special, face acest lucru fără a aștepta un răspuns. Așa că șeful îi spune replica să facă două ordine de retragere și pleacă imediat. Aceste mesaje ajung în replică a primi unde fiecare este afișat și asortat cu gestionarul corespunzător. În acest caz, Retrage execută retrage metoda din exemplul anterior și deduce suma solicitată din starea x. După ce se termină, actorul trece la următorul mesaj din coadă.

Deci, ce ajungem aici? În primul rând, nu mai trebuie să ne facem griji cu privire la blocarea și lucrul cu tipuri atomice / concurente. Mecanismul de încapsulare și de așteptare a actorului garantează deja siguranța firului. Și nu mai există așteptare, deoarece firele doar lasă mesajul și revin. Rezultatele pot fi livrate ulterior cu cere sau spune. Este simplu și sănătos.

Akka se bazează pe JVM și este disponibil atât în ​​Scala, cât și în Java. Deși acest articol nu dezbate Java vs Scala, potrivirea modelelor Scala și programarea funcțională ar fi foarte utile în gestionarea mesajelor de date ale actorului. Cel puțin vă poate ajuta să scrieți un cod mai scurt evitând parantezele și punctele și virgulele Java.