Project Loom: les threads virtuels Java sont proches

Il y a quelques jours, Ron Pressler a donné naissance à un article dans State of Loom , que seul le javiste le plus paresseux n'a pas pu trouver. L'article est vraiment bon, il contient beaucoup de métaphores intéressantes, que je vais utiliser sans scrupule maintenant sans référence à la source.

Pour ma part, je me suis permis par inadvertance d'exprimer un certain scepticisme, mais quand avec ce projet, Loom peut enfin être vraiment travaillé. Littéralement une heure plus tard, une réponse est venue de Ron lui-même - "et vous essayez!" . Eh bien, je devais essayer.

Ce qui était requis pour l'expérience:

  • JDK15 avec Project Loom
  • Terminal, comme IntelliJ et Gradle ont catégoriquement refusé de travailler avec un tel jeu

Pour commencer, j'ai décidé de regarder à quoi ressemblerait un exemple classique avec des flux de la documentation de Kotlin Coroutine . L'exemple est écrit en Kotlin, mais le réécrire en Java n'est pas difficile:

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());
    }
}

Nous commençons et nous assurons que l'exemple se bloque toujours, comme précédemment:

javac Main.java && java Main

Réécrivez maintenant l'exemple en utilisant les threads virtuels que Project Loom nous fournit:

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

Le résultat ne tarde pas à venir:

1000000

Combien de temps?
, .
-, Gil Tene, — .
-, Project Loom, . , — .

Mais en soi, cela ne m'a pas trop impressionné. Au final, avec des coroutines à Kotlin, le même résultat est facilement atteint.

Dans son article, Ron observe correctement que Kotlin a dû introduire la fonction delay (), qui permet à la coroutine de «s'endormir», car Thread.sleep () n'envoie pas la coroutine actuelle en veille, mais le thread de l'ordonnanceur actuel, dont il n'y en a pas beaucoup, généralement par le nombre CPU

Et Project Loom?

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

Résultat:

- 400K

Mais c'est déjà intéressant! Avec Project Loom, l'appel à Thread.sleep () peut distinguer s'il se trouve dans un thread normal ou dans un thread virtuel, et cela fonctionne différemment.

En soi, c'est très cool. Mais creusons un peu plus profondément:

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();
}

Ici, nous exécutons 10 tâches lentes et 10 tâches rapides. Les tâches rapides sont plus rapides que les lentes un million de fois, il serait donc logique de supposer qu'elles se terminent plus tôt.

Mais ce n'était pas là:

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


L'intuition ne fonctionne que tant que le nombre de tâches lentes est inférieur au nombre de cœurs de votre CPU.

La raison est simple - pour le moment, Project Loom utilise le ForkJoinPool habituel. En conséquence, malgré le fait que la documentation et l'idée indiquent que les flux virtuels sont «préventifs», ils se comportent actuellement de manière «coopérative». Comme les coroutines de Kotlin, en fait.

Il convient de noter que dans l'article susmentionné, Ron mentionne qu'il réfléchit également au comportement de préemtion forcée, comme dans les threads normaux. Mais jusqu'à présent, cela n'a pas été implémenté, car il n'est pas complètement clair comment un tel comportement est utile lorsqu'il peut y avoir des dizaines de milliers de threads. Cependant, dans Go 1.14, la préemtion forcée a été discrètement introduite .

Un appel de fonction, contrairement à Go, n'entraîne pas de changement de contexte. La suspension, comme à Kotlin, n'a pas non plus été prononcée. Mais vous pouvez le faire avec Thread.yield (), ou en appelant n'importe quelle fonction Java IO: System.out.println (""), par exemple.

Le calcul est simple - la plupart des programmes réels utilisent assez souvent le blocage des E / S. Et c'est son utilisation de Project Loom qui cherche à résoudre en premier lieu.

Quelques conclusions:


Je dois admettre que malgré mon scepticisme initial, Project Loom m'a impressionné positivement. Pour les utilisateurs Java réguliers, il promet un multithread léger sans avoir besoin de passer à une autre langue, en utilisant une bibliothèque ou un framework. Ce qui sonne déjà bien.

Mais la principale révolution, comme je l'attends, ce projet sera fait parmi les développeurs de bibliothèques qui doivent encore résoudre le problème de concurrence encore et encore et encore. Maintenant, avec la distribution de JDK15, ce problème peut être transféré à la JVM, comme ils le faisaient pour transférer l'optimisation de la mémoire (GC) et du code (JIT) vers la JVM.

Lien vers mon article d'origine si vous préférez lire en anglais.

All Articles