Alexey Soshin
Alexey SoshinStaff Software Engineer @ Deliveroo

Wirtualne wątki w Javie

Sprawdź, jak używać tzw. wirtualnych wątków w Javie oraz jak zwiększyć współbieżność swojego programu.
16.07.20204 min
Wirtualne wątki w Javie

Jakiś czas temu Ron Pressler opublikował artykuł zatytułowany „State of Loom”, który był lansowany przez wszystkich znaczących ludzi ze społeczności JVM — i to z samych dobrych powodów. Chociaż sama lektura była bardzo interesująca (osobiście bardzo podobała mi się metafora „taksówki”), to jednak byłem sceptycznie nastawiony do przedstawionego w nim rozwiązania. Ron rzucił mi więc wyzwanie — mam wypróbować je samemu. 

Oto, czego będziemy potrzebować:

  • Java15 Early-Access Build, który zawiera Project Loom
  • Naszego ulubionego narzędzia wiersza poleceń, ponieważ IntelliJ nic obecnie nie wie na temat Java15


Na początek postanowiłem zaimplementować podstawowe wyzwanie, które pojawia się w dokumentacji o współprogramach Kotlina: rozpoczęcie miliona wątków, które zwiększą licznik atomowy. Chociaż oryginalny kod został napisany w Kotlinie, to łatwo go przepisać przy użyciu składni Javy:

public class Main {
    public static void main(String[] args) {
        var c = new AtomicLong();
        for (var i = 0; i < 1_000_000; i++) {
            new Thread(() -> {
                c.incrementAndGet();
            }).start();
        }

        System.out.println(c.get());
    }
}


Skompiluj i uruchom:

javac Main.java && java Main
... Never completes


Widać, że przy użyciu zwykłych wątków, to nadal nie działa. Przepiszmy to teraz, używając „wirtualnych wątków” zapewnionych nam przez Project Loom:

for (var i = 0; i < 1_000_000; i++) {
    Thread.startVirtualThread(() -> {
        c.incrementAndGet();
    });
}

Wynik jest poprawny:

1000000


Ale wiem, że stać nas na więcej.

Ron w swoim artykule słusznie wskazuje na to, że Kotlin musiał wprowadzić funkcję delay(), która zawiesiłaby coroutine. Dzieje się tak, ponieważ użycie Thread.sleep() uśpiłoby również jeden z wątków, który planuje współprogramy, zmniejszając tym samym współbieżność. Jak wirtualne wątki sobie z tym radzą?

for (var i = 0; i < 1_000_000; i++) {
  Thread.startVirtualThread(() -> {
    c.incrementAndGet();
    try {
        Thread.sleep(1_000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
  });
}


Wynik:

Something around 400K on my machine


To właśnie wtedy rzeczy stają się interesujące — Thread.sleep() jest świadome bycia wywoływanym w wirtualnym wątku. Zawiesza ono więc kontynuację, ale nie wykonanie. To już samo w sobie jest niesamowite. Mimo wszystko przyjrzymy się dokładniej następującemu ćwiczeniu.

var threads = new ArrayList<Thread>();
var cores = 10;
for (var i = 0; i < cores; i++) {
    var t = Thread.startVirtualThread(() -> {
        var bestUUID = "";
        for (var j = 0; j < 1_000_000; j++) {
            var currentUUID = UUID.randomUUID().toString();
            if (currentUUID.compareTo(bestUUID) > 0) {
                bestUUID = currentUUID;
            }
        }
        System.out.println("Best slow UUID is " + bestUUID);
    });
    threads.add(t);
}

for (var i = 0; i < cores; i++) {
    var t = Thread.startVirtualThread(() -> {
        var bestUUID = UUID.randomUUID().toString();
        System.out.println("Best fast UUID is " + bestUUID);
    });
    threads.add(t);
}

for (Thread t : threads) {
    t.join();
}


Tak więc uruchamiamy 10 wolnych i 10 szybkich zadań. Intuicyjnie można by powiedzieć, że szybkie zadania zakończą się wcześniej, bo są przecież milion razy szybsze. Nie chodzi tu jednak o intuicyjność:

Best slow UUID is fffffde4-8c70-4ce6-97af-6a1779c206e1
Best slow UUID is ffffe33b-f884-4206-8e00-75bd78f6d3bd
Best slow UUID is fffffeb8-e972-4d2e-a1f8-6ff8aa640b70
Best fast UUID is e13a226a-d335-4d4d-81f5-55ddde69e554
Best fast UUID is ec99ed73-23b8-4ab7-b2ff-7942442a13a9
Best fast UUID is c0cbc46d-4a50-433c-95e7-84876a338212
Best fast UUID is c7672507-351f-4968-8cd2-2f74c754485c
Best fast UUID is d5ae642c-51ce-4b47-95db-abb6965d21c2
Best fast UUID is f2f942e3-f475-42b9-8f38-93d89f978578
Best fast UUID is 469691ee-da9c-4886-b26e-dd009c8753b8
Best fast UUID is 0ceb9554-a7e1-4e37-b477-064c1362c76e
Best fast UUID is 1924119e-1b30-4be9-8093-d5302b0eec5f
Best fast UUID is 94fe1afc-60aa-43ce-a294-f70f3011a424
Best slow UUID is fffffc24-28c5-49ac-8e30-091f1f9b2caf
Best slow UUID is fffff303-8ec1-4767-8643-44051b8276ca
Best slow UUID is ffffefcb-614f-48e0-827d-5e7d4dea1467
Best slow UUID is fffffed1-4348-456c-bc1d-b83e37d953df
Best slow UUID is fffff6d6-6250-4dfd-8d8d-10425640cc5a
Best slow UUID is ffffef57-c3c3-46f5-8ac0-6fad83f9d4d6
Best slow UUID is fffff79f-63a6-4cfa-9381-ee8959a8323d


Intuicja działa tylko w przypadku, gdy liczba cores jest mniejsza lub równa liczbie rdzeni na maszynie. Trochę się tego spodziewałem, ponieważ Project Loom do planowania wirtualnych wątków używa ForkJoinPool, co nie jest znowu takie złe. 

Pomimo że, jak stwierdzono w dokumentacji, wątki wirtualne podlegają wywłaszczaniu, to ich planowanie nie opiera się na wywłaszczaniu, podobnie jak w przypadku współprogramów. Nie widać tu przeplatania wykonania wątków znanego z modelu pracy wątków na poziomie systemu. Jednak należy zauważyć, że twórcy projektu rozważają wprowadzenie wywłaszczania, co mogło być interesującą opcją.

Jednym z oczywistych rozwiązań byłoby tu użycie Thead.yield(), co działałoby całkiem ok. Problemem jest jednak to, że takie wywołanie nie zwraca rezultatu. Dodatkowo nie mamy do dyspozycji słowa kluczowego suspend, znanego z Kotlina, które by pomogło w tym przypadku.

Poza tym każde wywołanie standardowych operacji IO w Javie również spowoduje przekazanie wykonania do innego wątku. Na przykład, użycie System.out.print (“ “) spowodowało przełączenie kontekstu. W każdej rzeczywistej aplikacji IO jest to tak powszechne, że i tak nie powinno to stanowić prawdziwego problemu.

Podsumowanie

Muszę przyznać, że Project Loom jest naprawdę niesamowity. Z pewnością przyda się wielu programistom, skutecznie zapewniając wszędzie lekką współbieżność bez potrzeby korzystania z danej biblioteki lub środowiska.

Jednak największą korzyścią dla twórców bibliotek jest fakt, że gdy Java15 osiągnie wystarczająco szerokie zastosowanie, obawy o współbieżność będzie można odłożyć na bok i w końcu zająć się rozwojem nowych funkcji.

Chyba właśnie takie są nasze oczekiwania wobec całej platformy, prawda? W ostatnim czasie było sporo pracy nad odśmiecaniem, optymalizacją kodu, a teraz czas na kolejny krok, czyli współbieżność.


Oryginał tekstu w języku angielskim możesz przeczytać tutaj

<p>Loading...</p>