Hilos virtuales con Project Loom 🔥

Hola Loom!

Thread.startVirtualThread(() -> {
    System.out.println("Hello, Loom!");
});

201907 LoomBlanket

Tools de prueba
  1. Esta es la versión de project loom de prueba

  2. https://jdk.java.net/loom/

  3. openjdk-19-loom+6-625_linux-x64_bin.tar.gz

Desde finales del 2017 se ha introducido una preview, para el uso de hilos virtuales, se supone que con esto daría un giro inesperado al mundo Java 🤔, tanto [1]Ron pressler como [2]Mark Reinhold están manteniéndonos al tanto de como va todo esto.

Una de las razones es el rendimiento que ofrecen, porque no invocan a los hilos del kernel del sistema aka native threads, dado que se crea un contexto especial, al parecer como comenta Ben Evans muy parecido a los antes existentes green threads.

[3]Ben Evans OpenJDK’s Project Loom aims, as its primary goal, to revisit this long-standing implementation and instead enable new Thread objects that can execute code but do not directly correspond to dedicated OS threads. Or, to put it another way, Project Loom creates an execution model where an object that represents an execution context is not necessarily a thing that needs to be scheduled by the OS. Therefore, in some respects, Project Loom is a return to something similar to green threads.

Se podría decir que no es un nuevo concepto, sino otra implementación.

virtualThreadStates

Project loom vs Project reactor ?

batman vs superman videojuego

Seguro que Batman y superman nunca trabajaron juntos? 🤙🏿

Se decia por ahí, que con project loom no haria falta programación reactiva, pero en realidad, ambas se pueden complementar.

Añadiendo que aun con el uso de hilos virtuales, crear un thread con código no es tan facil de leer, componer, la mayoria de las veces, ahí es un punto fuerte que nos da project reactor.

Olek Dokuka con Andrii Rodionov

Ejecutando un par de hilos virtuales

Hardware para este ejemplo 🔥
  1. Intel® Core™ i7-8750H CPU @ 2.20GHz × 12

  2. disco duro m2 xD no tendria nada que ver, hacemos todo en memoria.

  3. 32gb de ram

  4. Ubuntu 20.04.3 LTS 64 bits

Vamos a probar la jdk19, pero debemos habilitar una flag en la JVM, y unas configuraciones en el IDE, a la misma vez que eso le dice al compilador que habilite las nuevas features/previews.

- Project Structure

Usar tal cual como la imagen.

projectStructure

⚙ Settings

settingJavaCompilerjdk19 virtualThreads

Esta es la famosa flag

--enable-preview

- Run/Debug Configurations

Usar las opciones siguientes

opcionesJVM

Añadir también -XX:+AllowRedefinitionToAddDeleteMethods

Un hilo normal del OS

la constante COUNT en realidad equivale a 10 millones de hilos
@Test
@DisplayName("Executing normal threads")
void normalThread() throws InterruptedException {
    var result = new AtomicLong();
    var latch = new CountDownLatch(COUNT); (1)
    for (int i = 1; i <= COUNT; i++) { (2)
        final int index = i;
        new Thread(() -> { (3)
        	result.addAndGet(index);
        	latch.countDown();
        }).start();
    }
    latch.await(); (4)
    log.info("Result: {}", result.get());

    assertThat(result.get()).isEqualTo(RESULT);
}
1 La constante de 10_000_000.
2 Un ciclo for del tamaño de esa constante.
3 Un hilo típico de toda la vida, fire and forget no apto para producción.
4 Este latch sabe la cantidad de tiempo que debe esperar, sin invocar al vulgar get o join.

normalThread

osThread


Con el Scheduler de Simon Baslé

El fue el autor de este Scheduler llamado boundedElastic, para suplantar al deprecado elastic.

@Test
@DisplayName("Using subscribeOn operator with Schedulers.boundedElastic")
void boundedElastic() throws InterruptedException {
    var result = new AtomicLong();
    var latch = new CountDownLatch(COUNT);
    for (int i = 1; i <= COUNT; i++) {
        final int index = i;
        Mono.fromSupplier(() -> result.addAndGet(index))
                .subscribeOn(Schedulers.boundedElastic()) (1)
                .doOnTerminate(latch::countDown)
                .subscribe();
    }
    latch.await();
    log.info("Result: {}", result.get());

    assertThat(result.get()).isEqualTo(RESULT);
}
1 Seteando el boundedElastic de esta manera, todo el stream reactivo se afectará por el switcheo de contexto, gracias al operador subscribeOn, obligando el uso de ese Scheduler.

boundedEleastic

reactiveThreadBoundedElastic


Caso 1

newVirtualThreadPerTaskExecutor
@Test
@SneakyThrows
@DisplayName("Executing virtual thread using subscribeOn operator " +
		" with Schedulers.fromExecutorService and newVirtualThreadPerTaskExecutor")
void virtualThread()  {
	var result = new AtomicLong();
	var latch = new CountDownLatch(COUNT);
	for (int i = 1; i <= COUNT; i++) {
		final int index = i;
		Mono.fromSupplier(() -> result.addAndGet(index))
				.subscribeOn(Schedulers.fromExecutorService(
						Executors.newVirtualThreadPerTaskExecutor())) (1)
				.doOnTerminate(latch::countDown)
				.subscribe();
	}
	latch.await();
	log.info("Result: {}", result.get());

	assertThat(result.get()).isEqualTo(RESULT);
}
1 Seteamos nuestro hilo virtual

virtualThreadPerTaskExecutorCode

virtualThread1


Caso 2

newThreadPerTaskExecutor
@Test
@SneakyThrows
@DisplayName("Executing virtual thread using subscribeOn operator " +
		" with Schedulers.fromExecutorService and newThreadPerTaskExecutor- and factory")
void virtualThread2() {
	var result = new AtomicLong();
	var latch = new CountDownLatch(COUNT);
	for (int i = 1; i <= COUNT; i++) {
		final int index = i;
		Mono.fromSupplier(() -> result.addAndGet(index))
				.subscribeOn(Schedulers.fromExecutorService(Executors.newThreadPerTaskExecutor(
						Thread.ofVirtual()
								.name("newThreadPerTaskExecutor-") (1)
								.factory())))
				.doOnTerminate(latch::countDown)
				.subscribe();
	}
	latch.await();
	log.info("Result: {}", result.get());
	assertThat(result.get()).isEqualTo(RESULT);
}
1 Usando factory para setearlo al contructor un hilo virtual.

factoryVirtualThread

virtualThread2

Tabla de resumen

N# Hilos Tipo de hilo/Scheduler Tiempo aproximado

10 millones

OS thread/Un Thread Típico

~7 min 36 sec

10 millones

boundedElastic

~24 sec 933 ms

10 millones

Caso 1

~19 sec 681 ms

10 millones

Caso 2 + factory

~17 sec 378 ms

Un stream reactivo común y corriente no se queda tan atrás como me imaginaba 🤔 Interesante!!!

También que viendo los hilos virtuales en un principio parecen generar cierto pico en memoria y CPU, pero no por mucho tiempo eso si 🔥

resumenImagenes


Que tal con Vaadin ?

Por ejemplo si ajustamos nuestro combo, nos quedaría así

usandoHiloVirtual


Como medir el Throughput ?

El ejemplo anterior no demuestra mucho e incluso, pareciera que lo hacen igual 🤣de rápido.

Lo ideal seria hacer una prueba de carga mejor, con JMeter, artillery, gatling etc, para ver cuantas request pueden procesar estos paradigmas, se supone que de manera reactiva se procesarian/manejarian mayor cantidad de request, o el doble mejor dicho.

La rapidez de respuesta no es un valor por la cual guiarse.


Parámetros para desplegar en heroku

Para hacer el despliegue en heroku es necesario añadir las flags, también para BlockHound porque tenemos una jdk superior a la +Jdk13 y se nos queja

SettingsHeroku

Las propiedades las separamos con comas ,

AllowRedefinitionToAddDeleteMethodsEnabledPreview

-XX:+AllowRedefinitionToAddDeleteMethods --enable-preview

Heroku no rula

Personalmente el plan de heroku básico, ya no me llama la atención, y oracle saca una ventaja muy grande, pero eso sera en otro post.