programowanie równoległe i asynchroniczne w Javie 8

Kod równoległy, który działa na więcej niż jednym wątku, był kiedyś koszmarem wielu doświadczonych programistów, ale Java 8 przyniosła wiele zmian, które powinny znacznie ułatwić zarządzanie tą sztuczką zwiększającą wydajność.

strumienie równoległe

przed Javą 8 istniała duża różnica między kodem równoległym (lub równoległym) a kodem sekwencyjnym. Bardzo trudno było również debugować kod niesekwencyjny. Po prostu ustawienie punktu przerwania i przejście przez przepływ, jak zwykle, usunęłoby aspekt równoległy, co jest problemem, jeśli to jest przyczyną błędu.

na szczęście Java 8 dała nam Streamy, najwspanialszą rzecz dla programistów Javy od czasu bean. Jeśli nie wiesz, czym one są, Stream API umożliwia obsługę sekwencji elementów w materii funkcjonalnej. (Sprawdź nasze porównanie strumieni i LINQ. NET tutaj.) Jedną z zalet streamów jest to, że struktura kodu pozostaje taka sama: niezależnie od tego, czy jest sekwencyjny, czy równoległy, pozostaje tak samo czytelny.

aby Kod działał równolegle, po prostu użyj .parallelStream() zamiast .stream() (lub stream.parallel(), jeśli nie jesteś twórcą strumienia).

ale to, że jest to łatwe, nie oznacza, że równoległy kod jest zawsze najlepszym wyborem. Zawsze powinieneś rozważyć, czy użycie współbieżności dla Twojego fragmentu kodu ma jakikolwiek sens. Najważniejszym czynnikiem w tej decyzji będzie szybkość: używaj współbieżności tylko wtedy, gdy sprawia, że Twój kod jest szybszy niż jego sekwencyjny odpowiednik.

pytanie o prędkość

Kod równoległy uzyskuje swoją prędkość dzięki użyciu wielu wątków zamiast jednego, którego używa kod sekwencyjny. Decyzja o liczbie wątków do utworzenia może być trudnym pytaniem, ponieważ więcej wątków nie zawsze skutkuje szybszym kodem: jeśli użyjesz zbyt wielu wątków, wydajność Twojego kodu może się obniżyć.

istnieje kilka zasad, które powiedzą Ci, jaką liczbę wątków wybrać. Zależy to głównie od rodzaju operacji, które chcesz wykonać i liczby dostępnych rdzeni.

operacje intensywnie obliczeniowe powinny wykorzystywać liczbę wątków mniejszą lub równą liczbie rdzeni, podczas gdy operacje intensywne IO, takie jak kopiowanie plików, nie mają zastosowania dla procesora, a zatem mogą używać większej liczby wątków. Kod nie wie, który przypadek ma zastosowanie, chyba że powiesz mu, co ma zrobić. W przeciwnym razie domyślnie będzie to liczba wątków równa liczbie rdzeni.

istnieją dwa główne przypadki, w których przydatne może być uruchamianie kodu równolegle zamiast sekwencyjnie: czasochłonne zadania i zadania uruchamiane na dużych kolekcjach. Java 8 przyniosła nowy sposób obsługi tych dużych zbiorów, a mianowicie strumieni. Strumienie mają wbudowaną wydajność przez lenistwo: używają leniwej oceny, która oszczędza zasoby, nie robiąc więcej niż to konieczne. To nie jest to samo, co równoległość, która nie dba o zasoby, dopóki idzie szybciej. Więc dla dużych kolekcji, prawdopodobnie nie potrzebujesz klasycznej równoległości.

asynchroniczny

lekcje z JavaScript

jest to rzadkie zjawisko, że programista Java może powiedzieć, że nauczył się czegoś od patrzenia na JavaScript, ale jeśli chodzi o programowanie asynchroniczne, JavaScript faktycznie dostał to dobrze pierwszy. Jako podstawowy język asynchroniczny, JavaScript ma duże doświadczenie z tym, jak bolesne może być, gdy jest źle zaimplementowane. Zaczęło się od wywołań zwrotnych, a później zostało zastąpione obietnicami. Ważną zaletą obietnic jest to, że ma dwa „kanały”: jeden dla danych i jeden dla błędów. Obietnica JavaScript może wyglądać mniej więcej tak:

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

tak więc, gdy oryginalna funkcja ma pomyślny wynik, wywoływana jest funkcja f1, ale jeśli wystąpił błąd, wywoływana jest funkcja e1. Może to doprowadzić go z powrotem do udanej ścieżki (f2) lub spowodować inny błąd (e2). Możesz przejść od ścieżki danych do ścieżki błędów iz powrotem.

Java w wersji JavaScript promises nazywa się CompletableFuture.

CompletableFuture

CompletableFuture implementuje Zarówno interfejs Future, jak i CompletionStage. Future istniał już przed Java8, ale sam nie był zbyt przyjazny dla programistów. Wynik obliczeń asynchronicznych można było uzyskać tylko za pomocą metody .get(), która zablokowała resztę (przez większość czasu część asynchroniczna była dość bezcelowa) i trzeba było ręcznie zaimplementować każdy możliwy scenariusz. Dodanie interfejsu CompletionStagebyło przełomem, który umożliwił programowanie asynchroniczne w Javie.

CompletionStage jest obietnicą, a mianowicie obietnicą, że obliczenia zostaną ostatecznie wykonane. Zawiera kilka metod, które umożliwiają dołączanie wywołań zwrotnych, które będą wykonywane po zakończeniu. Teraz możemy obsłużyć wynik bez blokowania.

istnieją dwie główne metody, które pozwalają uruchomić asynchroniczną część kodu: supplyAsync jeśli chcesz coś zrobić z wynikiem metody, i runAsync jeśli nie.

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

wywołania zwrotne

teraz możesz dodać te wywołania zwrotne, aby obsłużyć wynik twojego supplyAsync.

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

.thenApply jest podobna do funkcji .map dla strumieni: wykonuje transformację. W powyższym przykładzie bierze wynik (5) i mnoży go przez 3. Następnie przekaże ten wynik (15) dalej w dół rury.

.thenAccept wykonuje metodę na wyniku bez przekształcania go. Nie zwróci również wyniku. Tutaj wydrukuje” wynik to 15 ” na konsoli. Można ją porównać do metody .foreach dla strumieni.

.thenRun nie używa wyniku operacji asynchronicznej, a także nic nie zwraca, po prostu czeka na wywołanie swojego Runnable, aż poprzedni krok zostanie zakończony.

Asynchronizowanie Twojego asynchronicznego

wszystkie powyższe metody wywołania zwrotnego są również dostępne w wersji asynchronicznej: thenRunAsync, thenApplyAsync itp. Te wersje mogą działać na własnym wątku i dają dodatkową kontrolę, ponieważ możesz powiedzieć, którego ForkJoinPool użyć.

jeśli nie używasz wersji asynchronicznej, wywołania zwrotne będą wykonywane w tym samym wątku.

gdy coś pójdzie nie tak

gdy coś pójdzie nie tak, do obsługi wyjątku używana jest metoda exceptionally. Możesz nadać mu metodę, która zwróci wartość, aby wrócić do ścieżki danych, lub rzucić (nowy) wyjątek.

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

Połącz i skomponuj

możesz połączyć wiele CompletableFutures za pomocą metody thenCompose. Bez niego wynik byłby zagnieżdżony CompletableFutures. To sprawia, że thenCompose i thenApply jak flatMap i map dla strumieni.

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

jeśli chcesz połączyć wynik dwóch CompletableFutures, będziesz potrzebował metody o nazwie thenCombine.

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

jak widać w powyższym przykładzie, wynik wywołania zwrotnego w thenCombine może być obsługiwany jak normalny CompletableFuture ze wszystkimi ulubionymi metodami CompletionStage.

wniosek

programowanie równoległe nie musi już być przeszkodą nie do pokonania w poszukiwaniu szybszego kodu. Java 8 sprawia, że proces jest tak prosty, jak to tylko możliwe, tak że każdy kawałek kodu, który mógłby z niego skorzystać, może zostać wciągnięty, kopiąc i krzycząc na wszystkie wątki, w wielordzeniową przyszłość, która jest w rzeczywistości tylko teraźniejszością. Przez co mam na myśli: jest to łatwe do zrobienia, więc spróbuj i przekonaj się o jego zaletach.



+