Project Loom: los hilos virtuales de Java están cerca

Hace unos días, Ron Pressler dio a luz un artículo en State of Loom , que solo el javista más vago no pudo encontrar. El artículo es realmente bueno, hay muchas metáforas interesantes en él, que tengo la intención de usar descaradamente ahora sin referencia a la fuente.

Por mi parte, inadvertidamente me permití expresar algo de escepticismo, pero cuando con este Project Loom finalmente se puede trabajar realmente. Literalmente, una hora después, recibió una respuesta del propio Ron: "¡y tú lo intentas!" . Bueno, tuve que intentarlo.

Lo que se requería para el experimento:


Para empezar, decidí ver cómo sería un ejemplo clásico con secuencias de la documentación de Kotlin Coroutine . El ejemplo está escrito en Kotlin, pero reescribirlo en Java no es difícil:

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

Comenzamos y nos aseguramos de que el ejemplo todavía se cuelgue, como antes:

javac Main.java && java Main

Ahora reescribe el ejemplo usando los hilos virtuales que Project Loom nos proporciona:

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

El resultado no se hace esperar:

1000000

¿Cuánto tiempo?
, .
-, Gil Tene, — .
-, Project Loom, . , — .

Pero en sí mismo no me impresionó demasiado. Al final, con las corutinas en Kotlin, se logra fácilmente el mismo resultado.

Ron en su artículo observa correctamente que Kotlin tuvo que introducir la función delay (), que permite que la rutina se "duerma", ya que Thread.sleep () no envía la rutina actual a dormir, sino el hilo del planificador actual, del cual no hay muchos, generalmente por el número UPC

¿Qué hay de 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();
    }
  });
}

Resultado:

- 400K

¡Pero esto ya es interesante! Con Project Loom, la llamada a Thread.sleep () puede distinguir si está en un hilo normal o en un hilo virtual, y funciona de manera diferente.

Esto en sí mismo es muy bueno. Pero profundicemos un poco más:

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

Aquí ejecutamos 10 tareas lentas y 10 tareas rápidas. Las tareas rápidas son más rápidas que las lentas un millón de veces, por lo que sería lógico suponer que se completan antes.

Pero no estaba allí:

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


La intuición funciona solo mientras el número de tareas lentas sea menor que el número de núcleos de su CPU.

La razón es simple: en este momento, Project Loom utiliza el habitual ForkJoinPool. Como resultado, a pesar de que la documentación y la idea indican que los hilos virtuales son "preventivos", en este momento se comportan de manera "cooperativa". Como las corutinas en Kotlin, en realidad.

Cabe señalar que en el artículo antes mencionado, Ron menciona que también reflexiona sobre el comportamiento de prematura forzada, como en los hilos normales. Pero hasta ahora esto no se ha implementado, ya que no está completamente claro cómo tal comportamiento es útil cuando puede haber decenas de miles de hilos. Sin embargo, en Go 1.14, la prematura forzada se introdujo en silencio .

Una llamada de función, a diferencia de Go, no da como resultado un cambio de contexto. Suspender, como en Kotlin, tampoco fue entregado. Pero puede hacerlo con Thread.yield (), o llamando a cualquier función Java IO: System.out.println (""), por ejemplo.

El cálculo es simple: la mayoría de los programas reales usan IO de bloqueo con bastante frecuencia. Y es su uso de Project Loom lo que busca resolver en primer lugar.

Algunas conclusiones:


Tengo que admitir que a pesar de mi escepticismo inicial, Project Loom me impresionó positivamente. Para los usuarios normales de Java, promete multihilo ligero sin la necesidad de cambiar a otro idioma, utilizando una biblioteca o marco. Lo que ya suena bien.

Pero la principal revolución, como espero, este proyecto se realizará entre los desarrolladores de bibliotecas que todavía tienen que resolver el problema de concurrencia una y otra vez. Ahora, con la distribución de JDK15, este problema puede transferirse a la JVM, ya que solían transferir la optimización de la memoria (GC) y el código (JIT) a la JVM.

Enlace a mi artículo original si prefiere leer en inglés.

All Articles