Nasza strona używa cookies. Dowiedz się więcej o celu ich używania i zmianie ustawień w przeglądarce. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

Fibers - programowanie wielowątkowe w Javie

Poznaj koncept Fibers, czyli “lekkich wątków”, które za jakiś czas zadebiutują w standardowej Javie.

Lubię słuchać ludzi. Szczególnie tych, którzy powodują, że moje postrzeganie świata/kodu się zmienia. Czasami mają na to jawny wpływ, czasami całkowicie przypadkowy. Tych pierwszych spotkałem w swojej karierze zawodowej nad podziw dużo, za co jestem wdzięczny. Drudzy to nie tylko “autorytety” z konferencji. Mogą to być również osoby, które stykają się z jakimś zagadnieniem po raz pierwszy i posiadają świeże spojrzenie na dany temat.

Tak było też ostatnio, kiedy mój kolega, dopiero co startujący w programistycznym biznesie, zapytał mnie, dlaczego jak chce zaszyfrować dane poufne, to w odpowiedzi otrzymuje CompletableFuture<byte []> zamiast zwyczajnej byte [], przez co musi sporo kombinować, żeby te zaszyfrowane dane dostać i zwrócić w odpowiedzi.


Dlaczego?

Taka była moja pierwsza myśl, kiedy usłyszałem to pytanie. Zacząłem wtedy opowiadać, że w ramach szyfrowania będziemy prawdopodobnie wołać zewnętrzny system, co przekłada się na wykonanie operacji IO. Jeżeli w tym momencie odepniemy się od tego wątku, to będziemy w sposób nieblokujący czekać na to, aż system zewnętrzny wyśle nam odpowiedź, a nasz wątek będzie mógł wrócić do puli dostępnych i zajmować się rzeczami ważniejszymi niż czekanie na odpowiedź od zewnętrznego serwera. Okrasiłem to jeszcze kilkoma technicznymi słowami, rysunkiem na tablicy i objaśnieniem ogólnego konceptu non blockingu. Moja cudownie budowana historia legła niestety w gruzach, kiedy ten sam kolega zadał kolejne proste pytanie:

No dobra, a czy Java nie może sama wiedzieć, że to jest operacja blokująca, jakoś to na tych wątkach pod spodem zaczarować, a my skupilibyśmy się na naszej logice biznesowej?

W skrócie moja odpowiedź brzmiała “aktualnie Java nie ma takiego mechanizmu i musimy sami takie sytuacje obsługiwać”. Dyskusja wewnątrz zespołu się zakończyła, natomiast w mojej głowie dopiero się rozpoczęła. Realna odpowiedź przyszła później, kiedy drugi kolega przedstawił mi koncept starszy ode mnie (nic trudnego) i od niego (tutaj to już szacun). Ten koncept posiadał niewinną nazwę Continuations.


Project Loom

Okazało się, że chłopaki od Javy właśnie implementują ten koncept. Jest on realizowany w ramach projektu o nazwie Loom. Jak wiadomo (nie takie rzeczy ze szwagrem po pijaku, potrzymaj mi piwo) zawsze może coś pójść nie tak i na koniec nie pozostaje skwitować tego w inny sposób niż “O Panie a kto tu Panu tak… zaimplementował“. Zupełnie inaczej wygląda to w implementacji zaproponowanej przez Alana Batemana i Rona Presslera.

Główna klasa, którą powinniśmy być zainteresowani to java.lang.Continuation. Podczas konstrukcji instancji tej klasy najważniejszym parametrem jest obiekt Runnable, przedstawiający blok kodu, który chcemy uruchomić.

Aby uruchomić przekazany blok kodu, należy wywołać metodę Continuation.run(). Metoda może zakończyć obliczenia w dwóch sytuacjach. Pierwsza jest oczywista: obliczenia kończą się, kiedy wykonane zostaną wszystkie instrukcje przekazane jako parametr konstruktora. Druga sytuacja jest ciekawsza, ponieważ obliczenia mogą zostać przerwane w trakcie, jeżeli w ramach obliczeń wywołana zostanie metoda Continuation.yield(). W takiej sytuacji aktualny stan obliczeń jest zapisywany i zostaje wznowiony od miejsca zatrzymania dopiero po kolejnym wywołaniu metody run(). Przykładowy kod ilustrujący użycie metody run() oraz yield():

ContinuationScope scope = new ContinuationScope("demo");

Continuation continuation = new Continuation(scope, () -> {   
       System.out.println("A");   
       Continuation.yield(scope);   
       
       System.out.println("B");   
       Continuation.yield(scope);   

       System.out.println("C");
});


while (!continuation.isDone()) {
       System.out.println("run...");   
       continuation.run();
}


W wyniku wywołania otrzymamy:

run...
A
run...
B
run...
C


Klasa Continuation określana jest przez twórców jako low-level API, czyli programiści Javy nie powinni jej używać (chociaż w aktualnej, ciągle rozwijanej wersji jest to klasa publiczna). Dla użytkowników Javy, czyli dla nas, przewidziana została inna wersja API.


Fiber

Główną klasą tego high-level API jest java.lang.Fiber. Ma ona odzwierciedlać “lekki” wątek. U tych, którzy znają Kotlina, mogła pojawić się w tym momencie myśl: o masz, “Korutyny” odkryli. Słuchajcie, jest lepiej, chłopaki implementują te lekkie wątki przy wsparciu wirtualnej maszyny, o czym w Kotlinie możemy tylko pomarzyć. Fiber w skrócie opakowuje blok kodu w Continuation oraz uruchamia go w ramach executora.

W świecie wątków, jeżeli jakaś operacja jest blokująca wątek, na którym była realizowana, jest “parkowany” za pomocą natywnego wywołania Unsafe.park() i bezczynnie czeka do momentu, aż ktoś go “odparkuje”. W realnym świecie taką sytuacja bardzo często widzimy na budowie, kiedy jeden robotnik kopie, a reszta kulturalnie czeka oparta o swoją łopatę.

Świat włókien (ang. fibers) dodaje trochę magii podczas przetwarzania blokującego kodu. Parkowanie włókna skutkuje wywołaniem metody Continuation.yield() i zawieszeniem wykonywanego bloku, natomiast ramki ze stosu są kopiowane i umieszczane na stercie, aby po zdjęciu blokady wrócić na stos i kontynuować przetwarzanie. W aktualnej wersji nie ma jeszcze takiej możliwości, ale twórcy mają w planach możliwość serializowania obiektu Continuation, dzięki czemu możliwe będzie na przykład kontynuowanie przetwarzania na innej instancji.


I to wszystkie różnice?

Jeżeli wrócimy do przykładu z budowy, to najważniejsze różnice można przedstawić następująco: wyobraźmy sobie, że robotnicy do kopania używają ciężkiego zajmującego dużo miejsca sprzętu, który muszą na siebie założyć. Przez to, że jest ciężki, to jeden robotnik nie jest w stanie zbyt długo pracować, dlatego co jakiś czas muszą się zmieniać. Ta zmiana trwa dobre kilka minut, ponieważ muszą zdjąć ten sprzęt i potem go założyć na drugiego pracownika. Dodatkowo duży rozmiar sprzętu powoduje, że w danej chwili może pracować tylko jeden robotnik. Tak właśnie działają wątki. Ich największymi wadami jest rozmiar oraz koszt przełączania się między nimi.

Włókna to taki nowszy model tego sprzętu. Jest dużo mniejszy, więc może w tym samym czasie kopać wielu robotników oraz lżejszy, więc nie trzeba go na siebie zakładać, a dzięki temu zmiana pracownika odbywa się dużo szybciej.

No ok, ale jak to rozwiązuje problem, od którego rozpocząłem ten artykuł? W tamtym przypadku używaliśmy przecież podejścia non-blocking. Ten problem rozwiązany jest dokładnie tak, jak chciał tego mój kolega: Java wie, że wywoływana jest operacja blokująca, jakoś to na tych wątkach (tutaj włóknach) pod spodem czaruje, a my pisząc swój kod skupiamy się na logice biznesowej a nie obsłudze nieblokujących wywołań. Kod mimo blokującej formy, dorównuje pod względem wydajności nieblokującym rozwiązaniom. Dzieje się tak dlatego, że specyfika lekkich wątków niweluje wady tych standardowych.

Dodatkowo posiada zalety blokującego kodu, czyli jest czytelny, prostszy do napisania, debugowania, monitorowania oraz profilowania. Te wszystkie elementy czynią ten koncept bardzo interesującym i moim zdaniem Project Loom zmienia spojrzenie na przetwarzanie blokujących operacji i może w przyszłości zmienić całkowicie postrzeganie wielowątkowości w Javie.


Więcej wkrótce

Ten artykuł nie wyczerpuje tematu lekkich wątków w Javie, a jedynie stanowi pewien wstęp do dyskusji. Niestety sam projekt jest w fazie CompletableFuture, czyli wyjdzie, ale nie wiadomo kiedy. Patrząc na repozytorium, chłopaki ostro cisną, więc może Java 13, a może nie. Całe szczęście mają komfort, bo teraz releasy odbywają się co pół roku, więc jak spóźnią się na Javę 13, to nie będą czekać w blokach startowych kilku lat, a “jedynie” 6 miesięcy.

Nie ma aktualnie jeszcze żadnej oficjalnej wersji typu early access, dlatego jeżeli ktoś chce samemu pobawić się projektem Loom, to musi ściągnąć kod źródłowy JVM-a i samemu go skompilować.

Motto na dziś: Słuchajcie ludzi, nie tylko tych określanych mianem “autorytetów”.


Przydatne linki

Główna strona projektu Loom
Repozytorium: hg clone
Prezentacja autorów

Zobacz kogo teraz szukają