Parallel en asynchroon programmeren in Java 8

Parallel code, dat is code die op meer dan één thread draait, was ooit de nachtmerrie van veel ervaren ontwikkelaars, maar Java 8 bracht veel veranderingen die deze prestatieverhogende Truc een stuk beter beheersbaar zouden moeten maken.

parallelle Streams

voor Java 8 was er een groot verschil tussen parallelle (of gelijktijdige) code en sequentiële code. Het was ook erg moeilijk om niet-sequentiële code te debuggen. Gewoon een breekpunt instellen en door de flow gaan zoals je normaal zou doen, zou het parallelle aspect verwijderen, wat een probleem is als dat is wat de bug veroorzaakt.

Gelukkig gaf Java 8 ons streams, het beste ding voor Java-ontwikkelaars sinds de bean. Als je niet weet wat ze zijn, maakt de Stream API het mogelijk om sequenties van elementen te behandelen in een functionele kwestie. (Bekijk hier onze vergelijking tussen streams en. net ‘ s LINQ.) Een van de voordelen van streams is dat de structuur van de code hetzelfde blijft: of het nu sequentieel of gelijktijdig is, het blijft net zo leesbaar.

om uw code parallel te laten lopen, gebruikt u gewoon .parallelStream() in plaats van .stream() (of stream.parallel() als u niet de maker van de stream bent).

maar omdat het eenvoudig is, betekent niet dat parallelle code altijd de beste keuze is. Je moet altijd overwegen of het zinvol is om concurrency te gebruiken voor uw stukje code. De belangrijkste factor in die beslissing zal de snelheid zijn: gebruik alleen concurrency als het uw code sneller maakt dan zijn sequentiële tegenhanger.

de Snelheidsvraag

parallelle code krijgt zijn snelheidsvoordeel door het gebruik van meerdere threads in plaats van de ene die sequentiële code gebruikt. Beslissen hoeveel threads te maken kan een lastige vraag zijn, omdat meer threads niet altijd resulteren in snellere code: als je te veel threads gebruikt, kan de prestatie van je code eigenlijk naar beneden gaan.

er zijn een paar regels die je vertellen welk aantal threads je moet kiezen. Dit hangt vooral af van het soort operatie dat u wilt uitvoeren en het aantal beschikbare kernen.

intensieve bewerkingen voor berekeningen moeten een aantal threads gebruiken die kleiner zijn dan of gelijk zijn aan het aantal kernen, terwijl IO-intensieve bewerkingen zoals het kopiëren van bestanden geen gebruik maken van de CPU en daarom een groter aantal threads kunnen gebruiken. De code weet niet welke zaak van toepassing is, tenzij je hem vertelt wat hij moet doen. Anders zal het standaard een aantal threads gelijk aan het aantal kernen.

er zijn twee belangrijke gevallen waarin het nuttig kan zijn om uw code parallel te draaien in plaats van sequentieel: tijdrovende taken en taken draaien op grote collecties. Java 8 bracht een nieuwe manier van omgaan met die grote collecties, namelijk met streams. Streams hebben ingebouwde efficiëntie door luiheid: ze maken gebruik van luie evaluatie die middelen bespaart door niet meer te doen dan nodig is. Dit is niet hetzelfde als parallellisme, dat niet geeft om de middelen, zolang het maar sneller gaat. Dus voor grote collecties heb je waarschijnlijk geen klassiek parallellisme nodig.

Async

lessen uit JavaScript

het is een zeldzame gebeurtenis dat een Java-ontwikkelaar kan zeggen dat ze iets geleerd hebben van het kijken naar JavaScript, maar als het gaat om asynchrone programmering, JavaScript had het eigenlijk eerst goed. Als een fundamenteel async taal, JavaScript heeft veel ervaring met hoe pijnlijk het kan zijn wanneer slecht geïmplementeerd. Het begon met callbacks en werd later vervangen door promises. Een belangrijk voordeel van promises is dat het twee “kanalen” heeft: een voor data en een voor fouten. Een JavaScript belofte zou er ongeveer zo uit kunnen zien:

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

dus als de oorspronkelijke functie een succesvol resultaat heeft, wordt f1 aangeroepen, maar als er een fout is gegooid, wordt e1 aangeroepen. Dit kan het terug brengen naar de succesvolle track (f2), of resulteren in een andere fout (e2). U kunt gaan van data track naar error track en terug.

de Java-versie van JavaScript promises heet CompletableFuture.

CompletableFuture

CompletableFuture implementeert zowel de Future als de CompletionStage interface. Future bestond al pre-Java8, maar het was op zich niet erg ontwikkelaarvriendelijk. Je kon alleen het resultaat van de asynchrone berekening krijgen door gebruik te maken van de .get() methode, die de rest blokkeerde (waardoor het async gedeelte meestal nogal zinloos is) en je moest elk mogelijk scenario handmatig implementeren. Het toevoegen van de CompletionStage interface was de doorbraak die asynchrone programmering in Java werkbaar maakte.

CompletionStage is een belofte, namelijk de belofte dat de berekening uiteindelijk zal worden gedaan. Het bevat een heleboel methoden waarmee u callbacks die zal worden uitgevoerd op die voltooiing bijvoegen. Nu kunnen we het resultaat behandelen zonder te blokkeren.

er zijn twee methoden waarmee je het asynchrone deel van je code kunt starten: supplyAsync als je iets wilt doen met het resultaat van de methode, en runAsync als je dat niet doet.

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

Callbacks

nu kunt u deze callbacks toevoegen om het resultaat van uw supplyAsyncaf te handelen.

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

.thenApply is vergelijkbaar met de functie .map voor streams: het voert een transformatie uit. In het voorbeeld hierboven neemt het resultaat (5) en vermenigvuldigt het met 3. Het zal dan dat resultaat (15) verder in de pijp passeren.

.thenAccept voert een methode uit op het resultaat zonder het te transformeren. Het zal ook niet terug een resultaat. Hier zal het afdrukken “het resultaat is 15” naar de console. Het kan worden vergeleken met de .foreach methode voor streams.

.thenRun gebruikt het resultaat van de Async-operatie niet en geeft ook niets terug, het wacht gewoon om zijn Runnable aan te roepen totdat de vorige stap is voltooid.

uw Async

alle bovenstaande callback methoden komen ook in een async versie: thenRunAsync, thenApplyAsync, enz. Deze versies kunnen draaien op hun eigen thread en ze geven je extra controle omdat je het kunt vertellen welke ForkJoinPool te gebruiken.

als u de async-versie niet gebruikt, zullen de callbacks allemaal op dezelfde thread worden uitgevoerd.

als er iets fout gaat

als er iets fout gaat, wordt de exceptionally methode gebruikt om de uitzondering af te handelen. Je kunt het een methode geven die een waarde retourneert om terug te komen op de data track, of een (nieuwe) uitzondering gooien.

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

combineer en stel

samen u kunt meerdere CompletableFutures kettinglijnen met behulp van de thenCompose methode. Zonder dit, zou het resultaat worden genest CompletableFutures. Dit maakt thenCompose en thenApply zoals flatMap en map voor streams.

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

Als u het resultaat van twee CompletableFutures wilt combineren, hebt u een methode nodig die gemakkelijk thenCombinewordt genoemd.

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

zoals u kunt zien in het voorbeeld hierboven, kan het resultaat van de callback in thenCombine worden behandeld als een normale CompletableFuture met al uw favoriete CompletionStage methoden.

conclusie

Parallel programmeren hoeft niet langer een onoverkomelijk obstakel te zijn in de jacht op snellere code. Java 8 maakt het proces zo eenvoudig als maar kan, zodat elk stukje code dat er mogelijk van kan profiteren, kan worden getrokken, schoppen en schreeuwen op alle threads, in de multi-core toekomst die, in feite, alleen de huidige dag. Waarmee ik bedoel: het is gemakkelijk te doen, dus probeer het eens en zie de voordelen voor jezelf.



+