programare paralelă și asincronă în Java 8

Cod paralel, care este un cod care rulează pe mai multe fire, a fost odată coșmarul multor dezvoltatori experimentați, dar Java 8 a adus o mulțime de schimbări care ar trebui să facă acest truc de stimulare a performanței mult mai ușor de gestionat.

fluxuri paralele

înainte de Java 8 a existat o mare diferență între codul paralel (sau concurent) și Codul secvențial. De asemenea, a fost foarte greu să depanați codul non-secvențial. Pur și simplu setarea unui punct de întrerupere și trecerea prin flux așa cum ați face în mod normal, ar elimina aspectul paralel, care este o problemă dacă aceasta este cauza bug-ului.

din fericire, Java 8 ne-a dat fluxuri, cel mai mare lucru pentru dezvoltatorii Java de la fasole. Dacă nu știți ce sunt, API-ul Stream face posibilă gestionarea secvențelor de elemente într-o chestiune funcțională. (Verificați comparația noastră între fluxuri și LINQ. net aici.) Unul dintre avantajele fluxurilor este că structura codului rămâne aceeași: fie că este secvențială sau concurentă, rămâne la fel de ușor de citit.

pentru a face Codul să ruleze paralel, pur și simplu utilizați .parallelStream() în loc de .stream(), (sau stream.parallel(), dacă nu sunteți creatorul fluxului).

dar doar pentru că este ușor, nu înseamnă că codul paralel este întotdeauna cea mai bună alegere. Ar trebui să vă gândiți întotdeauna dacă are sens să utilizați concurența pentru bucata dvs. de cod. Cel mai important factor în această decizie va fi viteza: utilizați concurența numai dacă vă face Codul mai rapid decât omologul său secvențial.

întrebarea de viteză

Codul paralel își obține avantajul de viteză prin utilizarea mai multor fire în locul celui unic pe care îl folosește codul secvențial. A decide câte fire să creați poate fi o întrebare dificilă, deoarece mai multe fire nu duc întotdeauna la un cod mai rapid: dacă utilizați prea multe fire, performanța codului dvs. ar putea scădea.

există câteva reguli care vă vor spune ce număr de fire să alegeți. Acest lucru depinde în mare parte de tipul de operațiune pe care doriți să o efectuați și de numărul de nuclee disponibile.

operațiile intensive de calcul ar trebui să utilizeze un număr de fire mai mic sau egal cu numărul de nuclee, în timp ce operațiile intensive IO, cum ar fi copierea fișierelor, nu au niciun folos pentru CPU și, prin urmare, pot utiliza un număr mai mare de fire. Codul nu știe ce caz este aplicabil decât dacă îi spui ce să facă. În caz contrar, va fi implicit la un număr de fire egal cu numărul de nuclee.

există două cazuri principale când poate fi util să rulați codul paralel în loc de secvențial: sarcini consumatoare de timp și sarcini rulate pe colecții mari. Java 8 a adus un nou mod de manipulare a acestor colecții mari, și anume cu fluxuri. Fluxurile au eficiență încorporată prin Lene: folosesc evaluarea leneșă care economisește resurse prin faptul că nu fac mai mult decât este necesar. Acest lucru nu este același lucru cu paralelismul, care nu-i pasă de resurse atâta timp cât merge mai repede. Deci, pentru colecțiile mari, probabil că nu aveți nevoie de paralelism clasic.

merge asincron

lecții de la JavaScript

este un eveniment rar ca un dezvoltator Java poate spune că au învățat ceva de la uita la JavaScript, dar atunci când vine vorba de programare asincron, JavaScript de fapt luat-o dreapta primul. Ca un limbaj fundamental asincron, JavaScript are o mulțime de experiență cu cât de dureros poate fi atunci când este implementat prost. A început cu callback-uri și ulterior a fost înlocuit cu promisiuni. Un beneficiu important al promisiunilor este că are două „canale”: unul pentru date și unul pentru erori. O promisiune JavaScript ar putea arata ceva de genul asta:

func.then(f1).catch(e1).then(f2).catch(e2);

deci, atunci când funcția originală are un rezultat reușit, f1 este apelat, dar dacă a fost aruncată o eroare, e1 va fi apelat. Acest lucru ar putea aduce înapoi la pista de succes (f2), sau duce la o altă eroare (E2). Puteți trece de la pista de date la pista de eroare și înapoi.

versiunea Java a promisiunilor JavaScript se numește CompletableFuture.

CompletableFuture

CompletableFuture implementează atât interfața Future, cât și interfața CompletionStage. Future exista deja pre-Java8, dar nu era foarte prietenos cu dezvoltatorii de la sine. Puteți obține rezultatul calculului asincron folosind metoda .get(), care a blocat restul (făcând partea asincronă destul de inutilă de cele mai multe ori) și trebuia să implementați manual fiecare scenariu posibil. Adăugarea interfeței CompletionStage a fost descoperirea care a făcut programarea asincronă în Java funcțională.

CompletionStage este o promisiune, și anume promisiunea că calculul se va face în cele din urmă. Acesta conține o grămadă de metode care vă permit să atașați callback care vor fi executate pe această finalizare. Acum putem gestiona rezultatul fără a bloca.

există două metode principale care vă permit să porniți partea asincronă a codului dvs.: supplyAsync dacă doriți să faceți ceva cu rezultatul metodei și runAsync dacă nu.

CompletableFuture.runAsync(() → System.out.println("Run async in completable future " + Thread.currentThread()));CompletableFuture.supplyAsync(() → 5);

Callback

acum Puteți adăuga aceste callback să se ocupe de rezultatul dumneavoastră supplyAsync.

CompletableFuture.supplyAsync(() → 5).thenApply(i → i * 3).thenAccept(i → System.out.println("The result is " + i).thenRun(() → System.out.println("Finished."));

.thenApply este similar cu funcția .map pentru fluxuri: efectuează o transformare. În exemplul de mai sus se ia rezultatul (5) și se înmulțește cu 3. Apoi va trece acel rezultat (15) mai jos pe conductă.

.thenAccept efectuează o metodă asupra rezultatului fără a-l transforma. De asemenea, nu va returna un rezultat. Aici se va imprima” rezultatul este 15 ” la consola. Acesta poate fi comparat cu metoda .foreach pentru fluxuri.

.thenRun nu utilizează rezultatul operației asincron și, de asemenea, nu returnează nimic, doar așteaptă să apeleze Runnable până când pasul anterior este finalizat.

Asincronizarea Asincroniei

toate metodele de apel invers de mai sus vin și într-o versiune asincronă: thenRunAsync, thenApplyAsync etc. Aceste versiuni pot rula pe propriul fir și vă oferă un control suplimentar, deoarece puteți spune ce ForkJoinPool să utilizați.

dacă nu utilizați versiunea asincronă, atunci callback-urile vor fi executate pe același fir.

când lucrurile merg prost

când ceva nu merge bine, metoda exceptionally este utilizată pentru a trata excepția. Puteți să-i dați o metodă care returnează o valoare pentru a reveni pe pista de date sau pentru a arunca o excepție (nouă).

….exceptionally(ex → new Foo()).thenAccept(this::bar);

combinați și Compuneți

puteți înlănțui mai multe CompletableFutures utilizând metoda thenCompose. Fără aceasta, rezultatul ar fi imbricat CompletableFutures. Acest lucru face thenCompose și thenApply ca flatMap și map pentru fluxuri.

CompletableFuture.supplyAsync(() -> "Hello").thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "World"));

dacă doriți să combinați rezultatul a două CompletableFutures, veți avea nevoie de o metodă numită convenabil thenCombine.

future.thenCombine(future2, Integer::sum).thenAccept(value → System.out.println(value));

după cum puteți vedea în exemplul de mai sus, rezultatul apelului invers în thenCombine poate fi tratat ca un CompletableFuture normal cu toate metodele preferate CompletionStage.

concluzie

programarea paralelă nu mai trebuie să fie un obstacol insurmontabil în căutarea unui cod mai rapid. Java 8 face ca procesul să fie cât se poate de simplu, astfel încât orice bucată de cod care ar putea beneficia de el, poate fi trasă, lovind și țipând pe toate firele, în viitorul multi-core care este, de fapt, doar în prezent. Prin care vreau să spun: este ușor de făcut, așa că încercați și vedeți avantajele sale pentru dvs.



+