Projeto Loom: os encadeamentos virtuais Java estão próximos

Alguns dias atrás, Ron Pressler deu à luz um artigo no State of Loom , que apenas os javistas mais preguiçosos não conseguiram encontrar. O artigo é realmente bom, há muitas metáforas interessantes, que vou usar sem escrúpulos agora sem referência à fonte.

Da minha parte, inadvertidamente, permiti-me expressar um pouco de ceticismo, mas quando com esse projeto o tear pode finalmente ser realmente trabalhado. Literalmente uma hora depois, uma resposta veio do próprio Ron - "e você tenta!" . Bem, eu tive que tentar.

O que foi necessário para o experimento:


Para começar, decidi analisar como seria um exemplo clássico com fluxos da documentação de Kotlin Coroutine . O exemplo está escrito em Kotlin, mas reescrevê-lo em Java não é 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());
    }
}

Começamos e garantimos que o exemplo ainda trava, como antes:

javac Main.java && java Main

Agora, reescreva o exemplo usando os threads virtuais que o Project Loom nos fornece:

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

O resultado não tardará a chegar:

1000000

Quão mais?
, .
-, Gil Tene, — .
-, Project Loom, . , — .

Mas, por si só, não me impressionou muito. No final, com as corotinas em Kotlin, o mesmo resultado é facilmente alcançado.

Ron, em seu artigo, observa corretamente que Kotlin teve que introduzir a função delay (), que permite que a corotina "adormeça", pois Thread.sleep () não envia a corotina atual para dormir, mas o encadeamento atual do programa, do qual não há muitos, geralmente pelo número CPU

E o Projeto 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

Mas isso já é interessante! Com o Project Loom, a chamada para Thread.sleep () pode distinguir se está em um encadeamento regular ou virtual, e funciona de maneira diferente.

Isso por si só é muito legal. Mas vamos nos aprofundar um pouco:

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

Aqui, executamos 10 tarefas lentas e 10 tarefas rápidas. As tarefas rápidas são mais rápidas que as lentas um milhão de vezes, portanto, seria lógico supor que elas sejam concluídas mais cedo.

Mas não estava 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


A intuição funciona apenas enquanto o número de tarefas lentas for menor que o número de núcleos da sua CPU.

O motivo é simples - no momento, o Project Loom usa o ForkJoinPool usual. Como resultado, apesar de a documentação e a idéia indicarem que os fluxos virtuais são "preventivos", no momento eles se comportam de maneira "cooperativa". Como as corotinas em Kotlin, na verdade.

Deve-se notar que, no artigo acima mencionado, Ron menciona que ele também reflete sobre o comportamento de preempção forçada, como nos tópicos normais. Mas até o momento isso não foi implementado, porque não está completamente claro como esse comportamento é útil quando pode haver dezenas de milhares de threads. No entanto, no Go 1.14, a preempção forçada foi introduzida silenciosamente .

Uma chamada de função, ao contrário de Go, não resulta em uma alternância de contexto. Suspend, como em Kotlin, também não foi entregue. Mas você pode fazer com Thread.yield () ou chamando qualquer função Java IO: System.out.println (""), por exemplo.

O cálculo é simples - a maioria dos programas reais usa IO de bloqueio com bastante frequência. E é o uso do Project Loom que procura resolver em primeiro lugar.

Algumas conclusões:


Devo admitir que, apesar do meu ceticismo inicial, o Projeto Loom me impressionou positivamente. Para usuários regulares de Java, ele promete multithreading leve, sem a necessidade de alternar para outro idioma, usando uma biblioteca ou estrutura. O que já parece bom.

Mas a principal revolução, como eu espero, esse projeto será feito entre desenvolvedores de bibliotecas que ainda precisam resolver o problema de concorrência de novo e de novo e de novo. Agora, com a distribuição do JDK15, esse problema pode ser transferido para a JVM, pois eles costumavam transferir a otimização de memória (GC) e código (JIT) para a JVM.

Link para o meu artigo original, se você preferir ler em inglês.

All Articles