Traçando um Paralelo: JVM Virtual Threads (Fibers), BEAM lightweight-threads, Actor-based Model e Paradigma Reativo

Marcelo M. Gonçalves
17 min readSep 25, 2021

--

Introdução

Para entendermos a relação entre as tecnologias citadas no título e os conceitos reativos, precisamos voltar no tempo e traçar um paralelo. A timeline histórica de acontecimentos tornará mais clara as interligações e complementos durante a evolução ao longo dos anos. O propósito para a existência de novas tecnologias baseia-se na necessidade de superação dos desafios impostos pela sua própria evolução.

Tecnologias nascem e morrem todos os dias. Algumas são abandonadas enquanto outras prosperam. Simplesmente, não bastaria abordarmos, como exemplo, o paradigma reativo com seus objetivos e origens sem entendermos as relações gerais referentes a sua concepção. Para alcançarmos determinado objetivo será necessário ir além de uma visão consolidada no presente, buscando acontecimentos ligados à história dos fatos.

“Aqueles que não podem lembrar o passado estão condenados a repeti-lo”George Santayana.

Para que tecnologias sejam concebidas, idealizadas e projetadas, precisamos que se predisponham a resolver algum problema. Para isto, precisamos do contexto observado onde a necessidade foi identificada dando início ao ciclo de implementação de uma nova tecnologia. Sejam adaptações ou novos conceitos tecnológicos, será necessário observar a base evolutiva do landscape (panorama/cenário) atual, sendo portanto o ponto de partida para o sucesso exploratório. Obrigatoriamente considerando o passado e o presente de forma a influenciar a tomada de decisão.

Os avanços em tecnologia representam necessidades durante a resolução de problemas

Modelo de Atores (Actor-based Model)

O modelo de atores originou-se em 1973, criado por Carl Hewitt (Peter Bishop e Richard Steiger) com o intuito de prover uma camada de abstração para escrita de aplicações distribuídas e concorrentes. Com o modelo de atores não precisamos nos preocupar em lidarmos com operações de baixo nível que bloqueiam recursos (I/O) nem com gerenciamento de threads, facilitando a composição de sistemas paralelos.

De acordo com Carl Hewitt, o modelo de atores sendo um modelo de concorrência computacional foi inspirado em física, relatividade geral e mecânica quântica. O modelo utiliza atores como unidades primitivas de concorrência, comunicando-se através do envio de mensagens assíncronas, removendo a necessidade de sincronização baseadas em bloqueios (locks) durante os fluxos de execução.

Desde então o modelo foi utilizado como base conceitual para sistemas concorrentes que necessitam ser resilientes, tolerante a falhas e altamente responsivos. Dentre as mais famosas/populares implementações bem sucedidas do modelo de atores podemos destacar: Akka Framework (para JVM) & Celluloid (para Ruby) e também as linguagens Erlang & Elixir (para BEAM — Erlang VM).

Atores vivem em um Actor-based System, uma espécie de estrutura hierárquica semelhante a uma árvore. Atores possuem um identificador global dentro do sistema, enquanto trocam e processam mensagens como meio de comunicação, utilizam seus mailboxes como uma espécie de fila de entrada para processamento das requisições, onde permanecem persistidas. O modelo de atores trata-se de modelo conceitual, definindo regras gerais na forma de como sistemas concorrentes deveriam comportar-se ao interagirem com outros em ambientes computacionais concorrentes.

Os atores são unidades primitivas de execução. Em sistemas seguindo este modelo, tudo são atores fisicamente transparentes, devendo conhecer somente os endereços dos mailboxes uns dos outros. As mensagens são recebidas como tarefas a serem realizadas, devendo ser processadas devolvendo um resultado. Como mencionado, todo o fluxo ocorre de forma assíncrona por meio do envio de mensagens.

De forma similar encontramos este comportamento em sistemas orientados a objeto, onde um objeto recebe uma mensagem com algo a ser feito (chamada a um método), processa algo com base nos parâmetros recebidos podendo devolver um retorno. A diferença entre eles é que atores são isolados e não compartilham recursos como memória, mantendo seu estado de forma privada e não sendo permitido que atores alterem diretamente o estado de outros.

Precisamos entender que, embora múltiplos atores possam rodar paralelamente, as mensagens recebidas em sua fila de execução serão processadas sequencialmente e uma por vez. Neste modelo, para que seja possível paralelizar a execução das mensagens, necessitaríamos criar mais atores enviando mensagens individuais para cada instância em processamento.

Durante o processamento de uma mensagem, atores podem criar outros atores supervisionando seu ciclo de vida hierarquicamente. O modelo de atores possibilitou ao Erlang introduzir o conceito de “let it crash” (deixe quebrar), referindo-se a não escrevermos nossos códigos de forma defensiva na tentativa de antecipar todos os possíveis problemas que poderiam ocorrer durante a execução.

A filosofia “let it crash” propõe a criação de supervisores para assistir os processos em execução cujo seu objetivo seria recriá-los com seu estado inicial em caso de falhas. Desta forma, deixando que as aplicações construídas utilizando o modelo de atores recuperarem-se mais facilmente após falhas (Fault-tolerance).

Em resumo, o modelo de atores possui prós: são fáceis de escalar, não compartilham estado e são tolerantes a falhas. E contras: seu mailbox pode transbordar e estar suscetível a deadlocks. Em aplicações do mundo real, vale ressaltar algumas características importantes sobre atores: não importa se o ator que estamos enviando uma mensagem está rodando localmente ou remotamente (em um cluster ou na nuvem), eles sempre serão fisicamente transparentes, naturalmente distribuídos, não bloqueantes (non-blocking) e seguro contra race conditions (thread-safe) no universo da computação concorrente.

Assincronismo, Concorrência e Paralelismo: CPU e os Cores

Os CPUs modernos estão acoplando cada vez mais núcleos como forma de avanço. Encontrar tecnologias projetadas para tirar vantagens destes recursos de hardware aumenta as chances das aplicações prosperarem. Aplicações atuais precisam estar preparadas para permitir executar nossos códigos concorrentemente e em ambiente multi-core. Devido ao alto custo, o uso de OS/Threads diretamente, ou através de OS/VM Wrappers, visando alcançar estas características não é o mais adequado.

Conceitualmente, concorrência e paralelismo são coisas diferentes. Concorrência concentra-se em realizar diversas tarefas intercaladas ao mesmo tempo a nível de software enquanto o paralelismo refere-se a paralelizar coisas literalmente a nível de hardware (necessita de CPU multi-core). Concorrência pode ser aplicada em ambientes single-core referindo-se a disputa pelo tempo de CPU para execução de tarefas concorrentes e independentes. Neste caso, as fatias de tempo disponíveis no processador são gerenciadas para que os programas em execução possam ter a oportunidade de executarem.

Threads foram criadas com o objetivo de reduzir a troca de contextos entre os processos rodando em cada núcleo do CPU, economizando recursos do resto do sistema. OS/Threads estão sempre relacionadas a um processo do sistema operacional, que por sua vez pode possuir diversas threads em execução. Quando instanciadas em processos diferentes, threads compartilham o tempo de CPU da mesma forma que OS/Process, possuindo seu próprio contexto/memória de hardware.

Paralelismo pode ser realizado em diferentes níveis. Através de múltiplas unidades de execução dentro de um pipeline em apenas um núcleo do CPU, em vários núcleos dentro do CPU, em diversos CPUs em uma única máquina ou ainda através de diversas máquinas.

Assincronismo pode ser definido quando a ordem de execução de tarefas, concorrentes ou paralelas, não pode ser determinada. Atividades paralelas também são concorrentes, porém não necessariamente exclusivas. A execução assíncrona em nossas aplicações permite utilizarmos threads de maneira mais eficiente, introduzindo conceitos como future/promisses e callbacks permitindo que ações futuras sejam completadas independente do tempo que podem levar, enquanto a operação está em andamento ela não bloqueia a thread, liberando-a para realizar outras tarefas.

O hardware disponível no momento da execução de uma aplicação impacta diretamente seu poder de concorrência e paralelismo. Incluindo desde a quantidade de núcleos de um CPU até a sua otimização de uso durante o processamento. Não basta ter hardware escalando verticalmente de forma infinita, é necessário instrumentar a execução dos nossos componentes de forma a otimizar recursos tanto do ponto de vista de concorrência quanto de paralelismo.

Erlang VM e as Lightweight-threads (Erlang-process)

Erlang surgiu em 1986, implementando o modelo de atores através do conceito de Erlang-process (processos Erlang) com supervisão e tolerância a falhas. O modelo descrito por Carl Hewitt popularizou-se com o Erlang sendo usado com sucesso na Ericsson na área de telecomunicações. Embora o sucesso tenha ocorrido na área de telefonia, o modelo de atores utilizado no Erlang não fica restrito a esta área. As aplicações atuais necessitam cada vez mais serem distribuídas e concorrentes e mesmo a tecnologia do Erlang VM possuindo cerca de 35 anos, nunca foi tão moderna para encararmos os requisitos de hoje. Maiores informações sobre este tema podem ser encontradas aqui.

Em 1998 Erlang & OTP tornaram-se Open Source.

Processos no BEAM (Erlang VM) referem-se a algo bastante leve (lightweight-threads), sendo agendados eventualmente e de forma assíncrona. Por possuírem sua própria stack, lightweight-threads reduzem a troca de contexto durante a execução de um processo, tornando o custo operacional baixo. Processos podem acessar dados compartilhados dentro de uma OS/thread (heavyweight process) através do wrapper gerenciado pelos schedulers do BEAM.

No contexto de computação paralela em ambientes multi-threaded e devido a natureza preemptiva do BEAM scheduler, torna-se possível executar frequentes mudanças de contexto (pela VM) favorecendo a execução de processos de curta duração, reduzindo a ocorrência de starvation e beneficiando-se da estratégia de agendamento work stealing. Neste contexto, é importante notarmos uma das principais vantagens encontradas nas linguagens funcionais (Erlang, Elixir): oferecer imutabilidade. Podendo rodar em ambiente paralelo sem os riscos oferecidos pela mutabilidade encontrada em paradigmas como orientação a objetos (OOP).

A comunicação inter-processo no BEAM é realizada através da troca de mensagens, eliminando problemas relacionados às Race Conditions.

Diferente de outras máquinas virtuais, como a JVM, Erlang VM foi pensada com computação concorrente em mente desde o início. Utilizando o BEAM lightweight process (LWP) é possível inicializar diversas user-level threads (green-threads) dentro de um único processo do sistema operacional (OS/Process). O gerenciamento fica por conta da máquina virtual localizada um nível acima do sistema operacional, permitindo que operações multi-task sejam efetuadas em user-level trazendo diversos benefícios de performance.

Curiosamente, referente ao nome da linguagem funcional Erlang (Ericsson Language), se cavarmos um pouco mais fundo e retrocedermos no tempo, ao ano de 1908, encontraremos referências ao matemático e engenheiro dinamarquês Agner Krarup Erlang, responsável pela criação da Engenharia de Tráfego e a teoria das filas cujo nome da linguagem foi batizada em sua homenagem.

Tendo Agner desenvolvido uma fórmula, chamada de Erlang B, com o objetivo de solucionar problemas de bloqueio na quantidade de linhas telefônicas interligadas às centrais de duas cidades vizinhas. No fim das contas, tudo está interligado e pertence a uma cadeia de evolução natural tecnológica. Assim, a origem do Erlang denota tempos mais remotos do que sua concepção, demonstrando que historicamente preocupações com paralelismo e concorrência sempre estiveram em primeiro plano.

JVM e as Threads Virtuais (Virtual Threads — Fibers)

Em 1995, era oficialmente lançado o Java. Inicialmente desenhado para rodar dentro dos navegadores web, em Applets. No ano de seu lançamento, a maior parte dos softwares da época era desktop-based, poucos rodavam em rede, portanto, Java/JVM não foi construído com uma arquitetura pensada para conceitos como computação distribuída, concorrência e paralelismo.

Diferente do Erlang VM, idealizado para trabalhar com switches telefônicos paralelamente e concorrentemente com baixa latência, a JVM era conceitualmente mais simples. No ano de sua concepção, os engenheiros não projetaram que CPUs do futuro poderiam evoluir, adquirindo mais núcleos de processamento, viabilizando a execução paralela de tarefas, sendo uma das deficiências da máquina virtual Java na medida em que o tempo foi passando.

Os engenheiros responsáveis pela plataforma Erlang, liderados por Joe Armstrong, posicionarem-se no futuro imaginando como CPUs modernos iriam evoluir em seu poder de processamento. Os desafios encontrados ao desenhar uma plataforma para lidar com diversas ligações simultâneas em telecomunicações permitiram a tecnologia encontrada no Erlang VM estar preparada para o cenário que enfrentamos atualmente. Do ponto de vista de maturidade, desde os anos de criação entre o BEAM (1986) e a JVM (1995), temos cerca de uma década de distância entre elas somente na arrancada.

Com o passar dos anos, a JVM evoluiu, tornando-se cada vez mais competitiva e acoplando novas features. Ainda no Java 5 (lançado em 2004), mesmo que em formato rudimentar, era adicionado o pacote java.util.concurrent, facilitando a programação concorrente/multi encadeada por meio dos Executors. Acompanhado do Framework Fork/join (ForkJoinPool), incluído a partir do Java 7, em 2011 na tentativa de melhorar o suporte para execução de tarefas em paralelo. Uma espécie de pool de threads sendo capaz de referenciar a quantidade núcleos do CPU do sistema operacional executando a JVM, podendo rodar uma OS/Thread em cada núcleo.

Para executar as tarefas em paralelo, o ForkJoinPool usa um pool de threads, que por padrão é igual ao número de núcleos disponíveis no OS/CPU rodando a Java Virtual Machine (JVM). Nesse caso, cada thread possui sua própria fila de destino duplo (deque) podendo armazenar as tarefas que serão executadas. Uma OS/thread (thread física) pode executar somente uma tarefa por vez (a tarefa presente em seu deque). Algoritmos para agendamento work-stealing (roubo de trabalho) são implementados para balancear (equilibrar) a carga de trabalho das threads. Utilizando algoritmos deste tipo, as threads que ficam sem tarefas para processar podem roubar (pegar) tarefas de outras threads que ainda estão ocupadas (removendo tarefas de seu deque).

O Java 8, lançado oficialmente em 2014, adicionou os lambdas como paradigma funcional. Visando adquirir a imutabilidade, necessária em computação distribuída. Agora poderíamos criar pipelines funcionais, com fluxos executando transformações e mesmo o suporte funcional ao Java sendo pequeno, diversos problemas relacionados à mutabilidade deixaram de existir.

A criação de threads sempre foi custosa. Mesmo um pool de schedulers distribuídos nos núcleos do CPU, o principal problema estava relacionado às chamadas bloqueantes executadas aos downstreams (dependências externas: APIs, DBs, I/O). O tempo de resposta (response time) de uma requisição deixava a thread, recurso escasso no OS, ociosa aguardando retorno e este tempo era desperdiçado.

Em 1997, no lançamento da versão 1.1 do Java, houve uma tentativa de utilização das lightweight-threads/user-level na JVM (aqui). Era incluída na JVM-layer uma implementação das green-threads. O resultado acabou não sendo satisfatório (aqui), apresentando problemas relacionados ao balanceamento do trabalho executado em máquinas multi-core, demonstrando comportamento ineficiente (aqui) se comparado ao scheduler realizado automaticamente pelo sistema operacional com threads físicas. Desta forma, sendo a feature removida em releases subsequentes em favor das kernel-threads.

Embora a JVM não tivesse evoluído a ponto permitir beneficiar-nos de processadores multi-core oferecendo alta performance/paralelismo/concorrência sem overhead, bibliotecas como Quasar (lançado em 2013) propunham-se a prover estas características para JVM-based Languages através das lightweight-threads (no Quasar chamadas de Fibers), semelhantes às encontradas no Erlang VM, Goroutines/Channels em GO, Coroutines no Kotlin e Actors no Akka. Por se tratarem de Libraries e rodarem na camada de aplicação do software, estão limitados a ela. Diferente do BEAM, onde os lightweight-threads executam a nível de VM/Runtime, podendo os processos serem encerrados (kill) individualmente sem afetar o restante da aplicação.

Juntamente com Java/JDK 15, lançado em 2020, foi apresentado o promissor Project Loom, sendo um esforço da comunidade OpenJDK, liderado por Ron Pressler (criador do projeto Quasar), cujo objetivo seria trazer para a JVM modelos de execução com alta concorrência e maior performance introduzindo as virtual threads (as Fibers do Quasar rebatizadas). Novidade ou não, virtual threads tratam-se das lightweight-threads, inicialmente introduzidas de forma bem sucedida aos processos no Erlang VM (BEAM) porém 34 anos depois.

Akka Framework e o Actor-based Model

Mesmo com toda a evolução envolvendo concorrência e paralelismo alcançada na JVM, ainda não era o bastante, era preciso uma nova camada de abstração. Em 2010 Jonas Bonér lançava a primeira versão (0.5) da plataforma Akka (free and open-source). A decisão de criar o Akka havia sido baseada na tentativa sem sucesso da inclusão do Actor-model como parte do Scala (2.1.7) em 2006 por Philipp Haller. Na versão de Philipp, threads compartilhavam memória e possuíam sincronização por meio de locks.

Akka tratava-se de uma implementação do modelo de atores de Carl Hewitt, inspirada no Erlang, porém destinada a rodar na JVM. Akka estava trazendo para aplicações JVM-Based (como Scala, Java) as lightweight-threads em formato de atores, visando simplificar a construção de aplicações distribuídas e concorrentes porém a nível de aplicação e não de Runtime/VM, sendo sua principal referência o Erlang VM (BEAM). Akka substituiu a versão do modelo de atores presente no Scala desde então.

Com Akka era possível a criação de atores em grande quantidade e em um único núcleo/processo da CPU com a JVM consumindo poucos recursos de hardware. Atores executam hierarquicamente, com supervisão para tolerância a falhas. Aplicações conseguem entregar um alto nível de concorrência e performance, executar em paralelo e de forma assíncrona, serem responsivas e altamente distribuídas. A programação assíncrona orientada a callbacks torna-se um padrão para aplicações Akka, sendo um passo importante em direção ao suporte ao paradigma reativo nas linguagens, além de dar origem a outras versões como Akka.NET. Atualmente Akka faz parte, juntamente com uma infinidade de outros produtos (Scala, Lagom, Play Framework), da plataforma Lightbend (antiga Typesafe).

Akka baseia-se na infraestrutura da JVM. Projetct Loom se propõe a adicionar virtual threads (fibers, continuations e tail-call elimination) nativamente a JVM. Fibers na JVM mudarão a forma de escrever aplicações em Java (JVM-Based Languages) novamente, permitindo que códigos imperativos possam ser escritos sem bloquear recursos de hardware. Quando uma virtual thread é suspensa, aguardando resposta, o recurso não é bloqueado até ser resumida (retomada) sua execução novamente pelos continuations (armazenando uma pequena stack do contexto de execução da thread). A partir disso, chamadas bloqueantes se tornam não bloqueantes, e a suspensão e continuação (resumed) do fluxo de execução é automaticamente gerenciado pelo scheduler responsável necessitando somente de um pequeno pool de OS/Threads para criar uma quantidade significativa de lightweight-threads.

Uma thread, seja virtual ou física, trata-se de um conceito relacionado ao hardware (infra — camada de VM) e precisa residir na camada adequada. Simplesmente, trata-se do caminho correto a seguirmos com a implementação das lightweight-threads, como realizado na Erlang VM (BEAM) desde o início. Como podemos observar, a chave para o sucesso está localizada na camada onde as lightweight-threads encontram-se implementadas: camada de aplicação no Akka vs camada de infraestrutura (VM) no Erlang vs infraestrutura na JVM — Loom. O esforço do projeto Loom (liderado por Ron Pressler) e pela tentativa da Oracle de absorver o conceito das virtual-threads para dentro da JVM acaba parecendo uma tentativa de fazer a coisa certa, mesmo que com anos de atraso, porém destacando cada componente envolvido no processo em seu devido lugar.

O modelo de atores (assíncrono) no Akka não permite a utilização das virtual-threads presentes na JVM, pois o tempo de espera no bloqueio do recurso de hardware acaba se tornando nocivo. Ao passo em que ao utilizar virtual threads o ator precisaria aguardar até que o processamento ocorra para poder prosseguir, ficando preso e consequentemente incorrendo em uma sobrecarga de sua mailbox, podendo causar atrasos inesperados no fluxo do processamento afetando a performance de seu funcionamento.

Desta forma, existem aqui duas vertentes conceituais: de um lado a presença das green-threads sendo adicionadas nativamente na JVM beneficiando o estilo de programação tradicional/síncrono e bloqueante, tornando-o mais performático, com melhor otimização de recursos e possibilitando a criação de APIs cada vez mais concorrentes. Para o Akka (rodando na JVM), ocorre um overlap (sobreposição) conflitando com sua implementação atual (realizada na camada de aplicação) e desta forma não tornando possível beneficiar-se das virtual-threads nativas da JVM. (aqui e aqui)

Ambos, Erlang VM e Akka implementam conceitualmente o Actor-based Model. Porém, na prática posicionam as lightweight-threads em camadas distintas. Quando observado em perspectiva, as trajetórias tanto do Akka quanto do Loom Project — JVM, facilmente detectamos as raízes do problema estando localizadas na camada incorreta do posicionamento das lightweight-threads. No fim das contas, o preço a ser pago será o conflito entre o princípio de funcionamento com bloqueios (JVM lightweight-threads) e a implementação utilizada no Akka (Async I/O). Deixando claro que a única forma de o Akka continuar funcionando seria não permitir a utilização das lightweight-threads (Loom), mantendo em funcionamento sua implementação original.

Diferente da época (2009) em que o Akka foi implementado, onde recursos de threads eram bastante escassos (sendo o principal direcionador para o modelo Non-blocking I/O), foi a partir do projeto Quasar (2013), e posteriormente o Loom — JVM que esse tipo de recurso (threads-OS) tornou-se barato, podendo ser facilmente criado aos milhões. Ao mesmo tempo em que seria custoso ao Akka Team realizar mudanças estruturais na camada de JVM na ocasião em que o Akka foi desenvolvido, podendo ter seguido o mesmo caminho realizado na Erlang VM (BEAM), naquela circunstância parecia ser o melhor caminho a ser seguido.

Independente da existência das virtual-threads na JVM, precisamos salientar a trajetória (e importância) do Akka e da jornada de seu criador (Jonas Bonér). Certamente, todo o esforço e trade-offs deixam como herança principal o modelo de programação reativa tal qual conhecemos atualmente, tendo sido originalmente baseado no Manifesto Reativo, iniciativa também creditada ao criador do Akka (em conjunto com Roland Kuhn) naquela mesma época com o objetivo de unificar e padronizar o paradigma/conceito reativo. Com todo o sucesso inquestionável do Akka Platform desde então, fica evidente de que apenas pelo fato de a JVM (na época) não possuir as lightweight-threads, não significou que não pudesse ser construído algo tão espetacular quanto as encontradas originalmente na Erlang VM (BEAM).

O Paradigma Reativo — Programação Assíncrona

A programação reativa possui o foco principal em trabalhar com a propagação de mudanças no fluxo de dados de forma assíncrona aumentando sua capacidade responsiva. Com o passar do tempo, mesmo com mais recursos à disposição, como Pool de Threads (paralelismo) e Libraries voltadas a melhorar a concorrência em ambientes multi-core, era preciso uma solução mais elegante para lidar com a escassez de recursos e threads. Foram diversas as implementações da especificação reativa (Reactive streams Specification), como forma de contornar as limitações dos fluxos de execução rodando em cima de heavy-threads do sistema operacional. Maiores detalhes podem ser encontrados aqui.

Os processos/threads, mesmo em CPUs com vários núcleos, eram caros. No mínimo, era então necessário não deixar uma thread aguardando por resposta (I/O, API call, Database Call) após efetuar chamadas bloqueantes. Neste momento ainda não tínhamos as virtual threads nativamente na JVM visando tornar o processo menos oneroso e barato, desta forma, a programação assíncrona (“reativa”) começou a ser adotada como tentativa de otimizar recursos de hardware (OS/threads).

Mesmo antes do manifesto reativo ter sido escrito (The Reactive Manifesto, Setembro de 2014), onde um dos criadores era Jonas Bonér (criador do Akka), houveram iniciativas de implementação do paradigma reativo (ReactiveX/RxJava (0.14.5) em 2013 e ReactiveX/RxScala (0.21.1) em 2014). Após a criação da especificação, originada pelo manifesto, também tivemos implementações populares e bem sucedidas (Project Reactor (2016)).

O propósito da especificação reativa foi promover uma iniciativa de definir um padrão para processamento de streams de dados de forma assíncrona, não bloqueante e com backpressure (contra pressão), como exemplo: para JVM em 2014. Em se tratando do Java, o Project Reactor, foi uma mistura de esforços entre a comunidade, liderada por Stephane Maldini e posteriormente patrocinado pela Pivotal, empresa por trás do ecossistema Spring. Reactor-core beneficiou-se de lições aprendidas com versões anteriores do RxJava/Scala e amadureceu. Consequentemente, Akka adicionou o Akka Streams como implementação oficial para fluxos reativos e o resto é história.

A programação reativa resolve diversos problemas, porém, o que há de errado com ela? Os três principais problemas são: lost control flow, lost context e viral. Ao definirmos nossos pipelines reativos precisamos utilizar funções como thenCompose/flatMap tornando o código confuso e difícil de ler (lost control flow), além disso como cada future/callback roda em sua thread isoladamente, torna o stack-trace pouco informativo (lost context). Finalmente, sempre que uma chamada (método ou função) retornar um Future/CompletableFuture todas as chamadas encadeadas a ele obrigatoriamente necessitam seguir este padrão, podendo ocasionar bloqueios temporais ou mesmo valores sendo atribuídos de maneira forçada, o que evidentemente não seria desejado (viral).

Algumas linguagens e bibliotecas tentaram lidar com os problemas dos pipelines reativos citados acima, como C# e ScalaZIO, provendo stack-traces mais ricos e informativos ajudando na recuperação da perda de contexto (lost context). Outro exemplo seria o async/await, incluído nativamente no C# e Javascript e como feature no Scala, auxiliando na mitigação da perda do controle de fluxo (lost control flow). Ainda assim, async/await possui limitações relacionadas ao problema três (viral), pois funções assíncronas que retornam Future/Callback podem chamar tanto funções síncronas quanto assíncronas. Portanto, as funções síncronas não podem chamar as assíncronas, dividindo o universo das funções em blue (azul) para síncronas e red (vermelho) para assíncronas.

O objetivo das lightweight-threads (ou virtual threads), adicionadas a JVM, não seria substituir a maneira como viemos criando fluxos reativos, nem sugerir jogar fora tudo que aprendemos sobre o universo reativo e voltarmos a escrever códigos imperativos e bloqueantes. A ideia seria fornecer novas abordagens abrindo novas possibilidades onde possamos combiná-las e beneficiar-nos. Atores e fibers/lightweight-threads são similares, porém cada qual executando dentro do contexto ao qual foram projetados. Podendo ser utilizadas em conjunto, ambas as tecnologias aumentarão as chances de criarmos aplicações cada vez mais poderosas.

--

--