Desenvolvimento em Java 2.0: Apresentando o Kilim

Uma estrutura de agente para simultaneidade em Java

A programação simultânea é fundamental para o desenvolvimento em Java™ 2.0, mas provavelmente não para a simultaneidade baseada em encadeamento. Andrew Glover explica por que os agentes ultrapassam encadeamentos para programação simultânea em sistemas de vários núcleos. Em seguida, ele apresenta o Kilim, uma estrutura de transmissão de mensagens baseada em agentes que junta a programação simultânea à distribuída.

Andrew Glover, Author and developer

Andrew GloverAndrew Glover é desenvolvedor, autor, palestrante e empresário com uma paixão por desenvolvimento orientado a comportamento, Integração Contínua e desenvolvimento de software Agile. É possível entrar em contato com ele em seu blog.



28/Set/2010

A depuração de defeitos não deterministas em aplicativos com vários encadeamentos é uma das atividades mais difíceis e frustrantes conhecidas pelos desenvolvedores de software. Por isso, como muitas pessoas, fiquei muito entusiasmado com a programação simultânea usando linguagens funcionais como a Erlang e a Scala.

Tanto a Scala como a Erlang empregam o modelo de agente, em vez de encadeamentos, na programação simultânea. As inovações em torno do modelo de agente não se limitam apenas a linguagens; o modelo de agente também é acessível em estruturas de agente baseadas em Java, como o Kilim.

A abordagem do Kilim para o modelo de agente é intuitiva e, como você verá em breve, a biblioteca torna a construção de aplicativos simultâneos muito fácil.

O desafio de vários núcleos

Em 2005, Herb Sutter escreveu um artigo hoje famoso, intitulado, "The Free Lunch is Over: A Fundamental Turn Toward Concurrency in Software". Nele, ele destrói a enganosa crença de que a Lei de Moore continuaria desencadeando velocidades cada vez mais altas do relógio da CPU.

Sutter antecipou o fim da "carona" que consistia em aumentar o desempenho de aplicativos de software através do uso de chips cada vez mais rápidos. Em vez disso, ele afirmou que a obtenção de um aumento significativo no desempenho dos aplicativos exigiria o aproveitamento de arquiteturas de chip de vários núcleos.

Como se pôde ver, ele estava certo. Os fabricantes de chips atingiram um limite, com as velocidades de chip se estabilizando, nos últimos anos, em torno de 3,5 GHz. A Lei de Moore está viva e ativa na arena dos chips com vários núcleos, e os fabricantes estão aumentando a quantidade de núcleos nos chips a uma velocidade nunca vista.

Sobre esta série

O panorama do desenvolvimento em Java passou por mudanças radicais desde que a tecnologia Java surgiu. Graças a estruturas livres maduras e a infraestruturas de implementação confiáveis para aluguel, agora é possível montar, testar, executar e manter aplicativos Java de maneira rápida e barata. Nesta série, Andrew Glover explora a gama de tecnologias e ferramentas que torna possível esse novo paradigma de desenvolvimento em Java.

Sutter observou também que a programação simultânea permitiria que os desenvolvedores se beneficiassem das arquiteturas de vários núcleos. No entanto, ele acrescentou que "precisamos desesperadamente de um modelo de programação de nível mais alto para a simultaneidade do que as linguagens oferecem atualmente".

O modelo de programação básico de linguagens, como a linguagem Java, é baseado em encadeamento. Embora os aplicativos com vários encadeamentos não sejam extremamente difíceis de serem criados, existem desafios para criá-los corretamente. O que é difícil em relação à programação simultânea é pensar em termos de simultaneidade com encadeamentos. Os modelos alternativos de simultaneidade surgiram nesses termos. Um modelo particularmente interessante e que está se tornando cada vez mais conhecido na comunidade Java é o modelo de agente.


O modelo de agente

O modelo de agente é uma forma diferente de modelar processos simultâneos. Em vez de encadeamentos interagindo através de uma memória compartilhada com bloqueios, o modelo de agente usa "agentes" que passam mensagens assíncronas usando caixas postais. Uma caixa postal, nesse caso, é semelhante a uma caixa postal na vida real — as mensagens podem ser armazenadas e recuperadas para serem processadas por outros agentes. Em vez de compartilhar variáveis na memória, a caixa postal efetivamente separa processos diferentes uns dos outros.

Os agentes atuam como entidades distintas e separadas que não compartilham memória para comunicação. De fato, os agentes só podem se comunicar via caixas postais. Não há bloqueios nem bloqueios sincronizados no modelo de agente, portanto as questões que surgem deles — como conflitos e o abominável problema de perda de atualização — não constituem um problema. Além disso, os agentes têm a finalidade de funcionar de maneira simultânea, e não de maneira sequenciada. Dessa forma, os agentes ficam muito mais seguros (bloqueios e sincronização não são necessários) e o modelo de agente em si identifica os problemas de coordenação. Basicamente, o modelo de agente facilita a programação simultânea.

O modelo de agente não é novo de maneira nenhuma, já faz algum tempo que ele existe. Algumas linguagens, como a Erlang e a Scala, baseiam seu modelo de simultaneidade em agentes e não em encadeamentos. Na verdade, o sucesso da linguagem Erlang em contextos empresariais (a Erlang foi criada pela Ericsson e tem um rico histórico no mundo das telecomunicações) tornou comprovadamente o modelo de agente mais popular e visível, e isso ajudou a torná-lo uma opção viável para outras linguagens. A Erlang é um brilhante exemplo de como a abordagem do modelo de agente é mais segura para a simultaneidade.

Infelizmente, o modelo de agente não é integrado à plataforma Java, mas está disponível em diversos formatos. A abertura da JVM para linguagens alternativas significa que é possível tirar proveito de agentes usando uma linguagem da plataforma Java, como a Scala ou a Groovy (consulte a seção Recursos para conhecer a biblioteca de agentes Groovy, a GPars). Além disso, é possível experimentar uma das bibliotecas baseadas em Java que ativam o modelo de agente, como o Kilim.


Agentes com o Kilim

O Kilim é uma biblioteca escrita em Java que incorpora o modelo de agente. No Kilim, os "agentes" são representados pelo tipo Task do Kilim. Task são encadeamentos leves; eles se comunicam com outras Task pelo tipo Mailbox do Kilim.

Mailbox pode aceitar "mensagens" de qualquer tipo. Por exemplo, o tipo Mailbox aceita java.lang.Object. Task podem enviar mensagens String ou até mesmo tipos de mensagens personalizadas — é você quem decide.

No Kilim, tudo é agrupado via assinaturas de método; se for necessário fazer algo simultaneamente, especificamos o comportamento em um método aumentando sua assinatura para lançar Pausable. Dessa forma, a criação de classes simultâneas no Kilim é tão fácil quanto implementar Runnable ou estender Thread em Java. É que toda a bagagem ligada ao uso de Runnable ou Thread (como a palavra-chave synchronized) é reduzida.

Por fim, a mágica do Kilim é possibilitada por um processo de postagem, chamado weaver, que altera o bytecode das classes. Métodos que contêm a cláusula Pausablethrows são processados no tempo de execução por um programador, que faz parte da biblioteca Kilim. O programador manipula um número limitado de encadeamentos do Kernel. Ele é capaz de tirar proveito desse conjunto para obter um número mais elevado de encadeamentos leves, que podem alternar entre contextos e podem ser inicializados rapidamente. A pilha de cada encadeamento é gerenciada automaticamente.

Essencialmente, o Kilim facilita e simplifica a criação de processos simultâneos: basta fazer a extensão a partir do tipo Task do Kilim e implementar o método execute. Depois de compilar suas classes recém-criadas capazes de simultaneidade, execute o weaver do Kilim nelas e pronto!

O Kilim pode parecer um pouco estranho no começo, mas ele compensa. O modelo de agente (e, dessa forma, o Kilim) facilita e torna mais segura a criação de objetos que agem assincronamente e que dependem de objetos similares. É possível fazer o mesmo com o modelo de encadeamento base do Java (como estender Thread), mas é mais complicado porque você será lançado de volta ao mundo de bloqueios e sincronização. Em poucas palavras: alterar seu modelo de programação simultânea para agentes facilita a codificação de aplicativos com vários encadeamentos.


Kilim em ação

No modelo de agente do Kilim, as mensagens são transmitidas entre processos através de uma Mailbox. Em diversos sentidos, é possível pensar em uma Mailbox como uma fila. Os processos podem inserir itens em uma caixa postal e também retirar itens de uma caixa postal. Isso é feito com e sem bloqueio (o que é bloqueado é o processo leve subjacente implementado pelo Kilim, não um encadeamento do Kernel).

Como exemplo do aproveitamento de caixas postais no Kilim, criei dois agentes (Calculator e DeferredDivision) que são extensões do tipo Task do Kilim. Essas classes funcionarão de maneira cooperativa e simultânea. O objeto DeferredDivision criará um dividendo e um divisor; no entanto, ele não tentará dividir os dois. Imagine que é custoso fazer a divisão e, consequentemente, o objeto DeferredDivision pedirá ao tipo Calculator para lidar com essa tarefa.

Os dois agentes se comunicam por uma instância Mailbox compartilhada que aceita um tipo Calculation. O tipo de mensagem é bastante simples — são fornecidos um dividendo e um divisor e, consequentemente, Calculator irá executar o cálculo e definir a resposta correspondente. Em seguida, Calculator colocará essa instância Calculation de volta na Mailbox compartilhada.

Cálculo

A Listagem 1 mostra o tipo Calculation simples. Você pode observar que esse tipo não exige nenhum código especial do Kilim. Na verdade, ele é apenas um bean de Java comum.

Listagem 1. Uma mensagem do tipo Calculation
import java.math.BigDecimal;

public class Calculation {
 private BigDecimal dividend;
 private BigDecimal divisor;
 private BigDecimal answer;

 public Calculation(BigDecimal dividend, BigDecimal divisor) {
  super();
  this.dividend = dividend;
  this.divisor = divisor;
 }

 public BigDecimal getDividend() {
  return dividend;
 }

 public BigDecimal getDivisor() {
  return divisor;
 }

 public void setAnswer(BigDecimal ans){
  this.answer = ans;
 }

 public BigDecimal getAnswer(){
  return answer;
 }

 public String printAnswer() {
  return "The answer of " + dividend + " divided by " + divisor +
    " is " + answer;	
 }
}

DeferredDivision

As classes específicas do Kilim entram em cena na classe DeferredDivision. Essa classe faz diversas coisas, mas em um nível alto seu trabalho é simples: criar instâncias de Calculation com números aleatórios (do tipo BigDecimal) e enviá-las ao agente Calculator. Além disso, essa classe também verifica a MailBox compartilhada para ver se há Calculations nela. Se uma instância Calculation recuperada tiver uma resposta, DeferredDivision irá imprimi-la.

Listagem 2. DeferredDivision cria divisores e dividendos aleatórios
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.Date;
import java.util.Random;

import kilim.Mailbox;
import kilim.Pausable;
import kilim.Task;

public class DeferredDivision extends Task {

 private Mailbox<Calculation> mailbox;

 public DeferredDivision(Mailbox<Calculation> mailbox) {
  super();
  this.mailbox = mailbox;
 }

 @Override
 public void execute() throws Pausable, Exception {
  Random numberGenerator = new Random(new Date().getTime());
  MathContext context = new MathContext(8);
  while (true) {
   System.out.println("I need to know the answer of something");
   mailbox.putnb(new Calculation(
     new BigDecimal(numberGenerator.nextDouble(), context), 
     new BigDecimal(numberGenerator.nextDouble(), context)));
   Task.sleep(1000);
   Calculation answer = mailbox.getnb(); // no block
   if (answer != null && answer.getAnswer() != null) {
    System.out.println("Answer is: " + answer.printAnswer());
   }
  }
 }
}

Como é possível ver na Listagem 2, a classe DeferredDivision é estendida a partir do tipo Task do Kilim que, essencialmente, emula o modelo de agente. Observe que essa classe também substitui o método execute de Task que, por padrão, lança Pausable. Assim, as ações de execute estarão sob o controle do programador do Kilim. Ou seja, o Kilim irá assegurar que execute aja de forma simultânea e segura.

Dentro do método execute, DeferredDivision cria instâncias de Calculation e as coloca na Mailbox. Ele usa o método putnb para fazer isso de modo não bloqueador.

Depois de preencher a mailbox, DeferredDivision entra em estado de suspensão — observe que não é o encadeamento do Kernel que está suspenso, mas sim o encadeamento leve gerenciado pelo Kilim. Quando o agente é ativado, como foi anteriormente mencionado, ele verifica a mailbox para ver se contém Calculations. Essa chamada também não é bloqueadora, o que significa que getnb pode retornar null. Se DeferredDivision encontrar uma instância Calculation e seu método getAnswer tiver um valor (ou seja, não for uma instância Calculation que ainda não foi processada pelo tipo Calculator), ele imprime o valor no console.

Calculadora

No outro lado da Mailbox está a Calculator. Assim como o agente DeferredDivision definido na Listagem 2, a Calculator também é estendida da Task do Kilim e implementa o método execute. É importante observar que ambos os agentes compartilham a mesma instância Mailbox. Eles não podem se comunicar com diferentes Mailboxes; precisam compartilhar uma instância. Assim sendo, ambos os agentes aceitam uma Mailbox classificada pelo seu construtor.

Listagem 3. Finalmente, o trabalhador de verdade: Calculadora
import java.math.RoundingMode;

import kilim.Mailbox;
import kilim.Pausable;
import kilim.Task;

public class Calculator extends Task{

 private Mailbox<Calculation> mailbox;

 public Calculator(Mailbox<Calculation> mailbox) {
  super();
  this.mailbox = mailbox;
 }

 @Override
 public void execute() throws Pausable, Exception {
  while (true) {			
   Calculation calc = mailbox.get(); // blocks
   if (calc.getAnswer() == null) {
    calc.setAnswer(calc.getDividend().divide(calc.getDivisor(), 8, 
      RoundingMode.HALF_UP));				
    System.out.println("Calculator determined answer");
    mailbox.putnb(calc);
   }
   Task.sleep(1000);
  }
 }
}

O método execute de Calculator, assim como de DeferredDivision, realiza loops contínuos procurando itens na Mailbox compartilhada. A diferença é que Calculator invoca o método get, que é uma chamada bloqueadora. Assim sendo, quando uma "mensagem" de Calculation de fato aparece, ela executa o cálculo de divisão necessário. Finalmente, Calculator coloca o Calculation modificado de volta na Mailbox (de maneira não bloqueadora) e faz uma pausa. As chamadas suspensas em ambos os agentes estão presentes somente para facilitar a leitura do console.


O weaver do Kilim

Anteriormente, mencionei que o Kilim funciona por manipulação de bytecode por meio do seu weaver. Trata-se simplesmente de um processo de postagem que é executado em suas classes depois que você as tiver compilado. Em seguida, o weaver acrescenta alguns códigos especiais nas várias classes e métodos em que o marcador Pausable é encontrado.

Chamar o weaver é simples. Por exemplo, na Listagem 4, estou usando Ant para chamar o Weaver. Tudo o que tenho que fazer é dizer ao Weaver onde as classes desejadas estão e onde colocar o bytecode resultante. Nesse caso, estou instruindo o Weaver a alterar as classes encontradas no diretório target/classes e gravar o bytecode resultante de volta no mesmo diretório.

Listagem 4. Ant chama o weaver do Kilim
<target name="weave" depends="compile" description="handles Kilim byte code weaving">
 <java classname="kilim.tools.Weaver" fork="yes">
  <classpath refid="classpath" />
  <arg value="-d" />
  <arg value="./target/classes" />
  <arg line="./target/classes" />
 </java>
</target>

Após o código ter sido alterado, fico pronto para começar a usar o Kilim no tempo de execução, desde que tenha incluído seu arquivo .jar em meu caminho de classe.


Kilim no tempo de execução

Colocar os dois agentes em ação é semelhante a disparar dois Threads normais no código Java. Usamos a mesma instância sharedMailbox compartilhada para criar e iniciar as duas instâncias do agente e, em seguida, chamar o método start para colocar os agentes em ação.

Listagem 5. Um executador simples
import kilim.Mailbox;
import kilim.Task;

public class CalculationCooperation {
 public static void main(String[] args) {
  Mailbox<Calculation> sharedMailbox = new Mailbox<Calculation>();

  Task deferred = new DeferredDivision(sharedMailbox);
  Task calculator = new Calculator(sharedMailbox);

  deffered.start();
  calculator.start();

 }
}

A execução desses dois agentes produz a saída mostrada na Listagem 6. Se você executar esse código, sua saída provavelmente será um pouco diferente, mas a sequência lógica das atividades estará alinhada. Na Listagem 6, DeferredDivision solicita cálculos e Calculator fornece uma resposta.

Listagem 6. Sua saída irá variar — os agentes não são deterministas
[java] I need to know the answer of something
[java] Calculator determined answer
[java] Answer is: The answer of 0.36477377 divided by 0.96829189 is 0.37671881
[java] I need to know the answer of something
[java] Calculator determined answer
[java] Answer is: The answer of 0.40326269 divided by 0.38055487 is 1.05967029
[java] I need to know the answer of something
[java] Calculator determined answer
[java] Answer is: The answer of 0.16258913 divided by 0.91854403 is 0.17700744
[java] I need to know the answer of something
[java] Calculator determined answer
[java] Answer is: The answer of 0.77380722 divided by 0.49075363 is 1.57677330

Conclusão

O modelo de agente facilita a programação simultânea, possibilitando um mecanismo mais seguro para a transmissão de mensagens entre processos (ou agentes). As implementações desse modelo variam de acordo com as linguagens e as estruturas. Sugiro que você verifique os agentes da linguagem Erlang e depois os da linguagem Scala. Ambas as implementações são bastante limpas, de acordo com as respectivas sintaxes.

Se você quiser usar agentes Java "sem enfeites", sua melhor aposta pode ser o Kilim ou uma estrutura semelhante (consulte a seção Recursos). Não há "carona" envolvida, mas uma estrutura baseada em agentes torna a programação simultânea e o aproveitamento de processos com vários núcleos muito mais fácil.

Recursos

Aprender

Obter produtos e tecnologias

  • GPars - Groovy Parallel Systems: introdução à estrutura baseada em agentes do Groovy para programação simultânea na plataforma Java.
  • Faça o download do Kilim: Uma estrutura de transmissão de mensagens para Java que fornece encadeamentos ultraleves e facilita a transmissão de mensagens de forma rápida, segura e sem cópias entre esses encadeamentos.

Discutir

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=486109
ArticleTitle=Desenvolvimento em Java 2.0: Apresentando o Kilim
publish-date=09282010