Parallel og asynkron programmering i Java 8

Parallel kode, som er kode, der kører på mere end en tråd, var engang mareridt for mange en erfaren udvikler, men Java 8 bragte mange ændringer, der skulle gøre dette præstationsfremmende trick meget mere håndterbart.

parallelle strømme

før Java 8 var der en stor forskel mellem parallel (eller samtidig) kode og sekventiel kode. Det var også meget svært at debug ikke-sekventiel kode. Blot at indstille et breakpoint og gå gennem strømmen som du normalt ville gøre, ville fjerne det parallelle aspekt, hvilket er et problem, hvis det er det, der forårsager fejlen.

heldigvis gav Java 8 OS streams, den største ting for Java-udviklere siden bønnen. Hvis du ikke ved, hvad de er, gør Stream API det muligt at håndtere sekvenser af elementer i en funktionel sag. (Se vores sammenligning mellem streams og. net ‘ S link her.) En af fordelene ved vandløb er, at strukturen af koden forbliver den samme: uanset om det er sekventielt eller samtidig, forbliver det lige så læsbart.

for at få din kode til at køre parallelt bruger du simpelthen .parallelStream() i stedet for .stream(), (eller stream.parallel(), hvis du ikke er skaberen af strømmen).

men bare fordi det er nemt, betyder det ikke, at parallel kode altid er det bedste valg. Du bør altid overveje, om det giver mening at bruge samtidighed til dit stykke kode. Den vigtigste faktor i denne beslutning er hastigheden: brug kun samtidighed, hvis det gør din kode hurtigere end dens sekventielle modstykke.

Hastighedsspørgsmålet

Parallel kode får sin hastighedsfordel ved at bruge flere tråde i stedet for den eneste, som sekventiel kode bruger. At beslutte, hvor mange tråde der skal oprettes, kan være et vanskeligt spørgsmål, fordi flere tråde ikke altid resulterer i hurtigere kode: hvis du bruger for mange tråde, kan ydelsen af din kode faktisk gå ned.

der er et par regler, der fortæller dig, hvilket antal tråde du skal vælge. Dette afhænger mest af den type operation, du vil udføre, og antallet af tilgængelige kerner.

beregningsintensive operationer skal bruge et antal tråde, der er lavere end eller lig med antallet af kerner, mens io-intensive operationer som kopiering af filer ikke har brug for CPU ‘ en og derfor kan bruge et højere antal tråde. Koden ved ikke, hvilken sag der gælder, medmindre du fortæller den, hvad du skal gøre. Ellers vil det som standard være et antal tråde svarende til antallet af kerner.

der er to hovedtilfælde, hvor det kan være nyttigt at køre din kode parallelt i stedet for sekventiel: tidskrævende opgaver og opgaver kører på store samlinger. Java 8 bragte en ny måde at håndtere de store samlinger på, nemlig med streams. Streams har indbygget effektivitet ved dovenskab: de bruger doven evaluering, som sparer ressourcer ved ikke at gøre mere end nødvendigt. Dette er ikke det samme som parallelisme, som ikke er ligeglad med ressourcerne, så længe det går hurtigere. Så for store samlinger har du sandsynligvis ikke brug for klassisk parallelisme.

Going Async

Lessons From JavaScript

det er en sjælden begivenhed, at en Java-udvikler kan sige, at de lærte noget ved at se på JavaScript, men når det kommer til asynkron programmering, fik JavaScript det faktisk rigtigt først. Som et grundlæggende async-sprog har JavaScript stor erfaring med, hvor smertefuldt det kan være, når det er dårligt implementeret. Det startede med tilbagekald og blev senere erstattet af løfter. En vigtig fordel ved løfter er, at den har to “kanaler”: en til data og en til fejl. Et JavaScript-løfte kan se sådan ud:

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

så når den oprindelige funktion har et vellykket resultat, kaldes f1, men hvis en fejl blev kastet, kaldes e1. Dette kan bringe det tilbage til det vellykkede spor (f2) eller resultere i en anden fejl (e2). Du kan gå fra dataspor til fejlspor og tilbage.

Java-versionen af JavaScript promises kaldes CompletableFuture.

CompletableFuture

CompletableFuture implementerer både Future og CompletionStage interface. Future eksisterede allerede pre-Java8, men det var ikke meget udviklervenligt af sig selv. Du kunne kun få resultatet af den asynkrone beregning ved at bruge .get() – metoden, som blokerede resten (hvilket gør async-delen temmelig meningsløs det meste af tiden), og du havde brug for at implementere hvert muligt scenario manuelt. Tilføjelse af CompletionStage – grænsefladen var gennembrudet, der gjorde asynkron programmering i Java brugbar.

CompletionStage er et løfte, nemlig løftet om, at beregningen til sidst vil blive udført. Den indeholder en masse metoder, der lader dig vedhæfte tilbagekald, der vil blive udført på denne færdiggørelse. Nu kan vi håndtere resultatet uden at blokere.

der er to hovedmetoder, der giver dig mulighed for at starte den asynkrone del af din kode: supplyAsync hvis du vil gøre noget med resultatet af metoden, og runAsync hvis du ikke gør det.

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

tilbagekald

nu Kan du tilføje disse tilbagekald for at håndtere resultatet af din supplyAsync.

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

.thenApply svarer til .map – funktionen for streams: den udfører en transformation. I eksemplet ovenfor tager det resultatet (5) og multiplicerer det med 3. Det vil derefter passere dette resultat (15) længere ned i røret.

.thenAccept udfører en metode på resultatet uden at omdanne det. Det vil heller ikke returnere et resultat. Her udskrives “resultatet er 15” til konsollen. Det kan sammenlignes med .foreach – metoden for streams.

.thenRun bruger ikke resultatet af async-operationen og returnerer heller ikke noget, det venter bare på at kalde dets Runnable, indtil det forrige trin er afsluttet.

Asyncing af din Async

alle ovenstående tilbagekaldsmetoder kommer også i en async-version: thenRunAsync, thenApplyAsync osv. Disse versioner kan køre på deres egen tråd, og de giver dig ekstra kontrol, fordi du kan fortælle det, hvilken ForkJoinPool du skal bruge.

hvis du ikke bruger async-versionen, udføres tilbagekaldene alle på samme tråd.

når ting går galt

når noget går galt, bruges exceptionally – metoden til at håndtere undtagelsen. Du kan give det en metode, der returnerer en værdi for at komme tilbage på datasporet eller kaste en (ny) undtagelse.

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

Kombiner og komponere

du kan kæde flere CompletableFutures ved hjælp af thenCompose – metoden. Uden det ville resultatet blive indlejret CompletableFutures. Dette gør thenCompose og thenApply som flatMap og map for streams.

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

hvis du vil kombinere resultatet af to CompletableFutures, skal du bruge en metode, der bekvemt kaldes thenCombine.

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

som du kan se i eksemplet ovenfor, kan resultatet af tilbagekaldelsen i thenCombine håndteres som en normal CompletableFuture med alle dine foretrukne CompletionStage metoder.

konklusion

Parallel programmering behøver ikke længere at være en uoverstigelig hindring i jagten på hurtigere kode. Java 8 gør processen så ligetil som muligt, så ethvert stykke kode, der muligvis kan drage fordel af det, kan trækkes, sparke og skrige på alle tråde, ind i den multi-core fremtid, der faktisk bare er i dag. Med hvilket jeg mener: det er let at gøre, så prøv det og se dets fordele for dig selv.



+