João Vitor Mallmann Programação Concorrente Segura em Java Florianópolis - SC 2006 / 1 João Vitor Mallmann Programação Concorrente Segura em Java Orientador: José Mazzucco Júnior Universidade Federal de santa Catarina Florianópolis - SC 2006 / 1 Sumário 1 Objetivo p. 4 1.1 Tema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . p. 4 1.2 Limitações do tema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . p. 4 1.3 Objetivo geral . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . p. 5 1.4 Motivação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . p. 5 1.5 Desenvolvimento do trabalho . . . . . . . . . . . . . . . . . . . . . . . . p. 5 2 Introdução p. 6 3 Threads Java versus Processos CSP p. 8 3.1 Primitivas de sincronização . . . . . . . . . . . . . . . . . . . . . . . . . p. 8 3.2 Estados de transição . . . . . . . . . . . . . . . . . . . . . . . . . . . . p. 9 4 Processos comunicantes em Java p. 12 4.1 Interface de processo . . . . . . . . . . . . . . . . . . . . . . . . . . . . p. 12 4.2 Interface de canal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . p. 13 5 Programação concorrente com CTJ 5.1 Criando seu processo em Java . . . . . . . . . . . . . . . . . . . . . . . 6 Composição de processos p. 15 p. 15 p. 17 6.1 Construtor de Composição Seqüencial - Seqüencial . . . . . . . . . . . . p. 17 6.2 Contrutor de Composição Paralelo - Paralelo . . . . . . . . . . . . . . . p. 18 6.3 Contrutor de Composição Paralelo baseado em Prioridade - PriParalelo p. 19 6.4 6.5 6.6 Contrutor de Composição Alternativo - Alternativo . . . . . . . . . . . p. 22 6.4.1 p. 23 Guardar Condicionais e Incondicionais . . . . . . . . . . . . . . Contrutor de Composição Alternativo baseado em Prioridade - PriAlternativo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . p. 24 Contrutor de Composição Aninhados . . . . . . . . . . . . . . . . . . . p. 25 7 Conclusão p. 28 Referências Bibliográcas p. 29 Referências p. 29 4 1 Objetivo 1.1 Tema A criação de sistemas em tempo real esta cada vez mais em evidencia, sendo necessário para isso a utilização de softwares que possam agir de forma concorrente, utilizando-se do conceito de threads pra conseguir tal artifício. Com a liberdade e a exibilidade dada pelas APIs (application programming interfaces), a geração de aplicações utilizando processamento multithread cou simplicado, resultando em sistemas defeituosos, devido ao perigo da ocorrência de condições indesejadas, tais como corridas de hazards, deadlock, livelock e starvation (negação de serviços innita). Desta forma, deve se tomar cuidado com cada tipo de sincronismo utilizado nas threads dentro da construção de uma aplicação, utilizando uma variedade de regras, normas e padrões de projeto. 1.2 Limitações do tema No trabalho a ser desenvolvido será o usado o pacote de Communicating Threads for Java (CTJ), threads que se comunicam em Java, que implementa o modelo CSP em Java, que inclui padrões de threads muito mais simples e conáveis que o modelo das threads de Java. O pacote CTJ fornece um pequeno conjunto de padrões de projeto que é suciente para programação concorrente em Java. Um importante avanço do CTJ é que o programador pode usar um grande conjunto de regras para eliminar os hazardas, deadlocks, livelock, starvation etc. durante o projeto e implementação. A teoria de processos seqüenciais de comunicação (Communicating Sequential Processes - CSP) oferece uma notação matemática pra descrever padrões de comunicação por expressões algébricas e contem evidencias formais para análise, vericando e eliminando 5 as condições indesejáveis na utilização de threads. Esse modelo foi provado bem-sucedido para criação de softwares concorrentes para tempo real e sistemas embarcados. 1.3 Objetivo geral Estudar a ferramenta existente que faz a vericação e validação do código de programas feitos utilizando CSP. Analisar passo a passo as vericações que o programa faz, vericando se respeita as condições impostas pela teoria de CSP, que eliminam os problemas comuns da utilização de threads, elaborando um manual de utilização. Desenvolver algum sistema clássico, jantar dos lósofos por exemplo, utilizando a teoria de CSP, vericando manualmente e com o programa de validação, se os resultados obtidos serão os mesmos e estarão de acordo com a teoria. Elaborando uma documentação sobre a utilização da teoria CSP em Java. 1.4 Motivação Devido a grande disseminação da linguagem Java, e sua vasta área de atuação, vericar se a utilização do pacote CTJ, que implementa a teoria de CSP, elimina o grande problema na utilização de múltiplas threads, que é a geração de hazardas, deadlocks, livelock, starvation etc. Pretendendo difundir a teoria de CSP, sua funcionalidade na pratica, e os custos para a sua utilização na resolução de problemas computacionais. 1.5 Desenvolvimento do trabalho Pesquisa e buscas de referências na literatura para o estudo da teoria de CSP, de Charles Antony Richard Hoare. Pesquisa e busca de referências na literatura para o estudo do pacote CTJ (Communicating Threads for Java). Desenvolvimento de uma aplicação utilizando o pacote CTJ, para comparações com códigos que utilizam multithread em Java, e vericação da aplicação com o programa validador, excluindo a existência de condições indesejáveis no código. Elaboração de manuais de utilização do pacote CTJ e de seu programa validador. 6 2 Introdução O modelo de thread de Java* fornece suporte multithreading dentro da língua e de sistemas runtime em Java. A sincronização de Java e o escalonamento são mal especicados gerando um desempenho de tempo real insatisfatório. A idéia de Java é deixar o sistema operacional especicar as regras para a sincronização e a escalonamento. Isto pode resultar em desempenhos diferentes utilizando-se sistemas operacionais diferentes. A teoria de CSP especica inteiramente o comportamento da sincronização e escalonamento das threads em alto nível de abstração, que é baseado em processos, em composições e em primitivas de sincronização. O pacote Communicating Threads for Java (CTJ) fornece um modelo conável de thread/CSP para Java. O núcleo do modelo de threads de Java é derivado dos conceitos tradicionais de multithreading. A literatura sobre threads e sincronização de threads é na maior parte sobre a compreensão do sistema operacional ou dos conceitos de baixo nível de multithreading. O raciocínio sobre aplicações utilizando threads, pode ser diferente para cada programa construído. Threads são menos abstratas e são a causa principal do aumento da complexidade das aplicações. A liberdade que dada pelas APIs (Application Programming Interface ) para a utilização de threads aumentam os perigos de race-hazards, dead lock, livelock, starvation etc. O programador deve ter muito cuidado e deve aplicar uma diversidade de regras, normas ou padrões de projeto. Analisar um programa multithread e eliminar os erros de estados das threads pode ser extremamente difícil utilizando um API. Métodos teóricos foram desenvolvidos para analisar o comportamento de aplicações multithread para eliminar o perigos gerados por ela. Infelizmente, o modelo de thread utilizado em Java possui uma grande lacuna entre a teoria e a prática. O pacote Communicating Threads for Java (CTJ) implementa o modelo de CSP (processos, composições e canais) em Java, que é um padrão mais simples e mais conável 7 de thread que o modelo utilizado por Java. O pacote de CTJ fornece um pequeno conjunto de padrões do projeto que é suciente para a programação concorrente em Java. Uma vantagem importante de CTJ é que o desenhador ou o programador pode aplicar um conjunto de regras e normas para eliminar race-hazards, dead lock, livelock, starvation, durante o projeto e execução. Compreender o conceito não requer muito conhecimento sobre a teoria de CSP. O pacote de CTJ constrói uma ponte entre a teoria e a prática de CSP. 8 3 Threads Java versus Processos CSP Os termos thread e processo são claramente relacionados. Um processo encapsula seus dados e métodos - da mesma forma que os objetos - e encapsulam também uma ou mais threads de controle. Em outras palavras, atribuir uma thread de controle separada a um objeto passivo criara um objeto ativo, tornando-o um processo. Um processo CSP é um objeto ativo cujas instruções são executadas por uma thread de controle que esta encapsulada no processo. De agora em diante, quando for mencionada a palavra processo, esta se referindo a um processo CSP. Canais são objetos passivos e essenciais entre processos, pelos quais processos podem enviar e receber mensagens. Variáveis devem ser mantidas dentro do espaço de memória do processo, e não precisam ser sincronizadas. Apenas dados de somente leitura podem ser compartilhados entre os processos. Processos podem iniciar processos, mas não podem controlar diretamente outro processo, exceto ele mesmo. Além de seguro, a depuração dos processos é menos penosa que seguir a execução de threads baseadas no modelo Java. O comportamento das threads Java é pouco especicado e fortemente dependente dos padrões adotados pelo sistema operacional. Então, o comportamento das threads em diferentes maquinas virtuais Java (JVM) pode variar, enquanto o comportamento dos processos devera ser semelhante em qualquer JVM. 3.1 Primitivas de sincronização Em java, um objeto pode ter mais de uma thread. Threads que podem operar dados compartilhados simultaneamente, devem ser sincronizadas para prevenir race-hazards que pode resultar em dados corrompidos ou estados inválidos (sistema trava). O usuário deve controlar cada thread utilizando variados métodos, os quais devem ser usados de forma 9 apropriada. Esse conjunto de métodos (ver as classes java.lang.Thread e java.lang.Object ) fornece um conceito básico e exível de mutilthreads. A clausula synchronized (ou construção de um monitor) protege a região critica onde se encontra os dados compartilhados permitindo que apenas uma thread tenha acesso a essa região de cada vez. Os métodos wait()/notify() fazem o enleiramento condicional das threads que necessitam utilizar as regiões criticas em comum. A construção de um monitor de sincronização envolve manter sobre observação mais que um método. Não é fácil determinar quais métodos ou regiões devem ser sincronizadas. Determinados padrões de projeto podem resolver esse problema, mas eles podem tornar seu programa mais complexo que o necessário. Além disso, inserir sincronização entre dois métodos corretamente pode ser difícil e suscetível a erros. Canais são objetos especiais que gerenciam a comunicação entre processos, de uma forma pré-denida. Canais são objetos passivos intermediários entre processos e controlam a sincronização, escalonamento, e transferência de mensagens entre processos. Comunicação por canais é considerado thread-safe (um trecho de código é thead-safe se funcionar corretamente durante uma execução multithreading), mais conável e rápida. Além disso, o programador estará livre de implementar a sincronização e o escalonamento. Canais CSP são inicializados sem buer e sincronizados de acordo com o principio do rendezvous, o processo escritor espera até que o processo leitor esteja pronto ou o processo leitor espera até que o escritor esteja pronto. Os canais CTJ permitem zero ou mais buers, de acordo com as suas necessidades. Pensar em processos é mais abstrato e fácil de entender para desenvolvedores que pensam em threads, como será discutido na próxima seção. A simplicidade de se utilizar canais ao invés de monitores é uma importante motivação para a utilização do conceito de CSP. 3.2 Estados de transição Para cara processador haverá apenas uma thread em execução em um determinado momento. Um sistema multiprocessador com n processadores poderá ter n threads executando simultaneamente. Entretanto um sistema uniprocessador pode executar múltiplas threads utilizando um escalonador. Os estados das threads e processos não precisam ser iguais, embora eles possam rodar no mesmo sistema. A seguir será ilustrada as diferenças entre estados de transição de threads e de processos. Estados de transição da thread. 10 Uma thread pode estar em um dos seguinte estados: - new: uma nova thread esta sendo criada; - running: thread em execução, de posse do processador; - ready: a thread esta pronta pra execução, apenas esperando a liberação do processador; - waiting: thread esta esperando um signal ou a que ocorra um evento para ir para o estado ready; - terminated: a thread terá terminado sua execução. O diagrama de transição de estados mostrado na gura 1 exemplica os modelo de transição das threads. Estados de transição de um processo. - Um processo pode estar em um dos seguintes estados: - instantiated: o processo pode ter sido criado ou terminou com sucesso, nenhuma thread esta atribuída ao processo; - running: a thread de controle foi alocada a um processo e então o processo foi ativado; - preempted: o processo foi preemptado por um de maior prioridade, o processo esta pronto para execução, mas não esta executando; - waiting: a thread de controle esta inativa ou bloqueada e esta esperando para ser 11 noticada; - garbage: o processo terminou e não será atribuído a mais nenhuma thread, o garbege collector de Java excluirá o objeto. O diagrama de transição de estados mostrado na gura 2 ilustra as transições de estados de um processo: A principal diferença entre os dois diagramas, é que mais de um processo pode estar rodando em paralelo, porém apenas uma thread poderá estar em execução para um processador. Em outras palavras, esse diagrama de estados do processo pode ser aplicado a qualquer processo e é independente do número de processadores. Enquanto o diagrama de transição das threads depende totalmente do número de processadores disponíveis. Entretanto, o diagrama de transição das thread está situado em um nível mais baixo que o diagrama de transição de processo. Assim, o usuário usa utiliza processos necessita apenas entender o diagrama de transição estados para os processos individuais. O diagrama de transição de estados de processo distingue entre escalonadores preemptivos e não preemptivos. Um processo que é preemptado por um processo de maior prioridade deve ser temporariamente interrompido e ir para o estado preempted. Um processo preemptado passará para o estado running caso não exista outro processo com prioridade maior e que esteja no estado running. 12 4 Processos comunicantes em Java A próxima seção descreverá as interfaces de processo e canal CSP, que foram implementadas no pacote CTJ. 4.1 Interface de processo Processos que executam paralelamente não vêem um ao outro. Cada processo enxerga seus canais e isso é tudo que eles precisam. Em Java, um processo pai cria um processo lho e inicia a execução do mesmo com o método run().A interface de processo CTJ é especicada por uma interface de processo passiva, especicando o método run(),e uma interface de processo ativa, especicando o conjunto de canais entrada/saída que serão usados pelo processo. A interface de processo pode ser derivada de modelos data-ow [2] (a comunicação é feita pela emissão das mensagens aos receptores), que são grafos rotulados e orientados, composto pelos processos (bolhas) e pelo uxo (arestas), representando processos e canais. Processos são conectados por arestas e elas especicam a direção das mensagens. A gura 3 mostra um grafo de uma interface de um processo ativo com um canal de entrada e outro de saída. 13 O nome do processo descreve sua funcionalidade. O nome do canal global expressa o tipo de mensagem que é trocada entre os processos. Na borda do processo, estão as especicações da entrada e saída do processo, os dados de entrada especicam o canal de entrada do processo, do qual será feita a leitura, e o uxo de saída especicará canal de saída, onde o processo escreverá. Canais de entrada e saída tem nomes locais dentro do processo, que podem ser diferentes dos globais, pois eles podem existir em contextos diferentes. Um canal global é utilizado por mais de um processo, o qual podem ser de entrada ou saída. Deve se ter cuidado quando um processo puder utilizar um canal para entrada e saída. 4.2 Interface de canal A interface de canais do pacote CTJ é simples, contendo uma interface para canais de entrada, que especica o método read(), e uma interface para canais de saída que especica o método write(), e o método getName() que retorna o nome do canal. Processos comunicando-se com outros processos utilizando leitura ou escrita nos canais compartilhados invocando o métodos read() e write(). Os canais CTJ permitem também múltiplas escritas e leituras. Comunicação via canais, implementada no pacote CTJ, fornece um estrutura de hardware independente e uma estrutura dependente. Ambas estruturas são conectadas por uma interface simples dos canais. Em outras palavras, os canais mapeiam o software em um hardware, como ilustrado na gura 4. Hardware independente: 14 A comunicação via canais fornece uma plataforma independente de estrutura, onde os processos podem ser alocados no mesmo sistema ou distribuídos em outros sistemas. Os processos nunca acessam diretamente o hardware, eles podem apenas se comunicar com seu ambiente por meio dos canais. Como resultado, os processos não sabem quais processos estão do outro lado do canal e não sabem que hardware está entre os dois. Hardware dependente: Canais podem estabelecer um link entre dois ou mais sistemas via algum hardware. Objetos especiais chamados link driver podem ser inseridos no canal. Os link drivers controlam o hardware e serão as únicas partes da aplicação que possuem dependência de hardware. A estrutura do link driver é denido como abstrata, e pode ser estendida conforme a necessidade, sem inuenciar o projeto ou as estruturas independentes de hardware. A estrutura do link driver também fornece tratamento de interrupções. Todos os processos que não utilizam link drivers são totalmente independentes de hardware. Os processos que utilizam link drivers podem ser mais ou menos dependentes do hardware, pois link driver representa diretamente o hardware. 15 5 Programação concorrente com CTJ Essa seção descreve como criar processos comunicantes e construtores compostos em Java, utilizando o pacote CTJ. 5.1 Criando seu processo em Java Um processo é denido pela interface csp.lang.Process. A interface csp.lang.Process dene o método público run(), ver código abaixo: public interface csp.lang.Process { public void run(); } Código 1, Interface de um processo passivo. Uma classe de processo deve implementar a interface csp.lang.Process, que é muito semelhante a interface java.lang.Runnable. O método run() implementa o corpo runnable do processo, que será invocado por outro processo e executara uma tarefa seqüencial. class MeuProcesso implements csp.lang.Process { // declarações locais public MeuProcesso( canais e parametros ) { 16 // construtor do processo } public void run() { // fazer alguma coisa } } Código 2, exemplo de uma classe de processo. O construtor do processo especica o nome do processo, interfaces dos canais de entrada e saída, e os parâmetros para inicialização do processo. Não mais que um processo pode invocar o método run() ao mesmo tempo. O método run() pode implementar atividades de tempo real e só é permitido começar a execução se os recursos necessários estiverem disponíveis, para um funcionamento conável. Na instanciação de um processo, o construtor deve congurar todos os recursos, como os canais de entrada e saída e o parâmetros, antes que o método run() seja chamado. 17 6 Composição de processos Basicamente, processos são executados quando o método run() é invocado. O processo que o chamou espera até o método run() retornar com sucesso. MeuProcesso processo = new MeuProcesso(); // cria um processo processo.run(); // executa o processo A teoria de CSP descreve composições comuns nas quais os processos são executados, ou seja, processos podem executar em seqüência ou em paralelo. Nessa seção será apresentado as seguintes construções de de composições: Seqüencial, Paralelo, PriPararelo, Alternativo e PriAlternativo. 6.1 Construtor de Composição Seqüencial - Seqüencial O construtor de composições seqüencial executa apenas um processo por vez. O construtor termina quando todos os processos tiverem terminado sua execução. O construtor de composições seqüencial é criado pela classe Sequential. O objeto seqüencial é um processo. Sequential seq = new Sequential(Process[] processos); O argumento processos é um array de processos. O construtor inicia quando invocado seu método run(). seq.run(); O exemplo a seguir mostra uma composição seqüencial de três processos. Sequential seq = new Sequential(new Process[] { new Processo1(interfaces de canal), 18 new Processo2(interfaces de canal), new Processo3(interfaces de canal) }); seq.run(); O Processo2 irá rodar após o Processo1 ser executado com sucesso, e o Processo3 após a execução bem sucedida do Processo2. O processo seq é bem sucedido quando todos os processos foram executados com sucesso. Novos processos podem ser adicionados, em tempo de execução, no m lista: seq.add(new Processo4(..)); ou ainda: seq.add(new Process[] { new Processo4(..), new Processo5(..) }); E novos processos podem ser inseridos em uma determinada posição na lista: seq.insert(processo, index); Podem ser removidos da lista de processos: seq.remove(processo); Esses métodos podem ser utilizados somente fora do construtor. Apenas o processo pai pode executá-los, apenas quando o processo construtor não estiver em execução, o processo deve estar no estado instantiated. Essas restrições asseguram a conabilidade e segurança para o construtor. 6.2 Contrutor de Composição Paralelo - Paralelo O construtor de composições paralelas executa processos em paralelo. O construtor termina quando todos os processos forem terminados. O construtor é criado pela classe Parallel, sendo esse objeto um processo. Parallel par = new Parallel(Process[] processos); O argumento processos é um array de processos. O construtor é inicializado quando o método run() é invocado. O exemplo seguinte mostra uma composição em paralelo de três processos. 19 Parallel par = new Parallel(new Process[] { new Processo1(interfaces de canal), new Processo2(interfaces de canal), new Processo3(interfaces de canal) }); par.run(); Os processos Processo1, Processo2 e Processo3 serão executados em paralelo. Cada um terá uma thread de controle separada com a mesma prioridade de execução. O processo par será nalizado com sucesso, se todos seus processos internos forem bem sucedidos. Novos processos podem ser inseridos a lista em tempo de exucução: par.add(new Processo4(..)); ou ainda: par.add(new Process[] { new Processo4(..), new Processo5(..) }); Da mesma forma, processos podem ser removidos: par.remove(processo); 6.3 Contrutor de Composição Paralelo baseado em Prioridade - PriParalelo O construtor de prioridades priparalelo estende o construtor de composição paralela, porém agora com prioridades. A cada processo do construtor priparalelo será dado uma prioridade, enquanto que os processos do construtor de composição paralela recebiam a mesma prioridade. O primeiro processo da lista receberá a maior prioridade, e o último receberá a menor prioridade da lista de processos. O objeto priparalelo é um processo. Atualmente, o número máximo de prioridade por objeto priparalelo é 8, onde 7 são para os processos e um é reservado para as tarefas (task ) idle, skip e garbage collector (ainda não implementado). Processos priparalelos podem ser inicializados aninhados, assim, podendo aumentar o número de processos com prioridades. Os processos são executados por prioridade, tais prioridades foram denidas pela 20 ordem de inserção na lista de processos, não sendo possível fazer a edição das mesmas para os processos já existentes. A classe PriParallel cria um construtor paralelo baseado em prioridade. PriParallel pripar = new PriParallel(Process[] processos); O argumento processos é uma array de processos. O construtor é iniciado pela chamada do método run(). pripar.run(); O exemplo seguinte mostra uma composição paralela de três processos: PriParallel pripar = new PriParallel(new Process[] { new Processo1(interfaces de canal), // prioridade 0 new Processo2(interfaces de canal), // prioridade 1 new Processo3(interfaces de canal) // prioridade 2 }); pripar.run(); Os processos Processo1, Processo2 e Processo3 serão executados em paralelo com prioridades sucessivas. O Processo1 (de índice 0) tem a maior prioridade (0). Todos os processos da lista que possuem índice 6 ou maior, compartilham a menor prioridade (6). O processo pripar termina com sucesso quando todos os processos internos são executados com sucesso. Para aumentar o número máximo de prioridades é possível criar novos processos priparelelos aninhados ao processo já criado. O exemplo seguinte mostra um construtor priparalelo, sendo 49 o número máximo de prioridades. PriParallel pripar = new PriParallel(new Process[] { new PriParallel(new Process[] // prioridade 0 { Process1_1(interfaces de canal), // prioridade 0.1 .. // prioridade 0.2-6 21 Process1_7(interfaces de canal) // prioridade 0.7 }), new PriParallel(new Process[] // prioridade 1 { Process2_1(interfaces de canal), // prioridade 1.1 .. // prioridade 1.2-6 Process2_7(interfaces de canal) // prioridade 1.7 }), new PriParallel(new Process[] { idem }), // prioridade 2.1-2.7 new PriParallel(new Process[] { idem }), // prioridade 3.1-3.7 new PriParallel(new Process[] { idem }), // prioridade 4.1-4.7 new PriParallel(new Process[] { idem }), // prioridade 5.1-5.7 new PriParallel(new Process[] { idem }) // prioridade 6.1-6.7 }); pripar.run(); Novos processos podem ser adicionados em tempo de execução: pripar.add(new Processo4(..)); ou ainda: pripar.add(new Process[] { new Processo4(..), new Processo5(..) }); Processos podem ser inseridos em determinada posição da lista de processos (recebendo a prioridade de tal índice): pripar.insert(processo, index); Processos podem ser removidos: pripar.remove(processo); A ordem das prioridades será atualizada com a inserção ou remoção de um processo. 22 6.4 Contrutor de Composição Alternativo - Alternativo O construtor de composição alternativa é composto por guardas, sendo cada guarda um processo. Assim que um guarda ca pronto, ele é executado pelo processo principal. O construtor da composição alternativa é denida pela classe Alternative. Alternative alt = new Alternative(Guard[] guardas); O argumento guardas é um array de objetos guardas. Um objeto guarda é uma instancia da classe Guard. O processo construtor é iniciado pelo mentodo run(). O exemplo seguinte mostra uma composição alternativa para três processos: Alternative alt = new Alternative(new Guard[] { new Guard(canal1, new Processo1(canal1, ..)), new Guard(canall2, new Processo2(canal2, ..)), new Guard(canal3, new Processo3(canal3, ..)) }); alt.run(); O processo alt espera pelo menos um dos guardas estar pronto, mas termina quando um dos processos for selecionado e nalizado com sucesso. O guarda que possui o processoi será selecionado quando o canali estiver pronto. Sendo o canali o canal de entrada do processoi. Se mais que um guarda estiver pronto para executar eles serão selecionados se forma randômica. Novos guardas podem ser inseridos em tempo de execução: alt.add(new Guard(canal4, new Processo4(canal4, ..)); ou ainda: alt.add(new Guard[] { new Guard(canal4, new Processo4(canal4, ..), new Guard(canal5, new Processo5(canal5, ..) }); 23 E podem ser removidos também: alt.remove(guarda); 6.4.1 Guardar Condicionais e Incondicionais O objeto guarda controla um processo, que estará pronto quando o construtor receber a primeira ocorrência de comunicação no canal de entrada desse processo. O processo é controlado por entradas, ou seja, apenas os canais de entrada podem ser monitorados pelos processos. O controle dos canais de saída não foi implementada por que isso resultaria em uma penalidade na performance do canal. Um novo objeto guarda pode ser declarado da seguinte forma: Guard guarda = new Guard(canal, new Process(canal,..)); O guarda se torna true quando o argumento canal (interface de entrada) está pronto e tem dados disponíveis para leitura. O guarda em si não é um processo, mas um objeto passivo que controla os processos. O objeto alternativo (principal) verica se todos os guardas estão disponíveis e espera pelo menos até que um canal que pronto, então o processo que pertence ao guarda ativado pode ser selecionado e executado. Um guarda habilitado que sempre é executado no construtor alternativo é chamado de incondicional. Um guarda também pode ser condicional, isto é, estará habilitado (participará do construtor alternativo) se alguma condição for verdadeira, ou estará desabilitado (não participa do construtor) e o processo não é executado. boolean conditicao = true; Guard guarda = new Guard(conditicao, canal, new Process(canal,..)); Se a condição for verdadeira o guarda vericará o canal, porém, se for falsa, o guarda não executará e o processo não será selecionado. Se o processo for selecionado, deverá ler os dados do canal. Um guarda declarado como new Guard(true, canal, new Process(canal,..)); é igual a new Guard(canal, new Process(canal,..)); A condição de execução do guarda pode ser modicada pelo método setEnabled(). 24 guarda.setEnabled(false); 6.5 Contrutor de Composição Alternativo baseado em Prioridade - PriAlternativo A classe PriAlternative cria um construtor de composição alternativo baseado em prioridade. A classe PriAlternative estende a classe Alternative e substitui o mecanismo de escolha randômica por um baseado em prioridade. O objeto prialternativo é um processo. O construtor prialternativo é semelhante do alternativo. PriAlternative prialt = new PriAlternative(Guard[] guardas); O argumento guardas é um array de objetos Guarda. Um objeto guarda é uma instância da classe Guard. O construtor prialternativo é inicializado pelo método run(). prialt.run(); O exemplo a seguir mostra uma composição prialternativa para três processos. PriAlternative prialt = new PriAlternative(new Guard[] { new Guard(canal1, new Processo1(canal1, ..)), new Guard(canal2, new Processo2(canal2, ..)), new Guard(canal3, new Processo3(canal3, ..)) }); O processo prialt espera até pelo menos um guarda estar pronto, mas termina com sucesso quando um dos processos internos é selecionado e nalizado com sucesso. O guarda com o processoi será selecionado quando o canali estiver pronto. Sendo o canali o canal de entrada do processoi. Se mais de um guarda estiver pronto, o guarda de maior prioridade (menor índice) será selecionado. O processo pertencente ao guarda selecionado será executado. Novos guardas podem ser adicionados em tempo de execução: prialt.add(new Guard(canal4, new Processo4(canal4, ..)); ou ainda: prialt.add(new Guard[] 25 { new Guard(canal4, new Processo4(canal4, ..), new Guard(canal5, new Processo5(canal5, ..) }); Um guarda pode ser inseridos por índice da lista: prialt.insert(guarda, index); Sendo a prioridade do guarda inserido igual ao seu índice, e as prioridades dos guardas com índice maior serão incrementadas. Podendo remover os guardas: prialt.remove(guarda); 6.6 Contrutor de Composição Aninhados Os processos de composição Seqüencial, Paralela, PriParalela, Alternativa e PriAlternativa podem possuir processos de composição aninhados. Para instânciar dois construtores seqüenciais rodando em paralelo: Process processo = new Parallel(new Process[] { new Sequential(new Process[] { new Processo1(interfaces de canal), new Processo2(interfaces de canal) }), new Sequential(new Process[] { new Process3(interfaces de canal), new Process4(interfaces de canal) }) 26 }); process.run(); Ou dois construtores paralelos executando em seqüencia: Process processo = new Sequential(new Process[] { new Parallel(new Process[] { new Process1(interfaces de canal), new Process2(interfaces de canal) }), new Parallel(new Process[] { new Process3(interfaces de canal), new Process4(interfaces de canal) }) }); process.run(); Da mesma forma, construtores alternativos podem ser internos a outro construtor: Process processo = new Sequential(new Process[] { new Parallel(new Process[] { new Process1(..), new Process2(..) }), new Alternative(new Guard[] 27 { new Guard(true, canal1, new Processo3(canal1, ..)), new Guard(false, canal2, new Processo4(canal2, ..)) new Guard(canal4, new Sequential(new Process[] { new Processo5(canal4, ..), new Processo6(..) })) }), new Parallel(new Process[] { new Processo7(..), new Processo8(..) }) }); process.run(); 28 7 Conclusão O pacote da Communicating Threads for Java (CTJ) é uma implementação do modelo CSP resultando em construtores baseados em processos, composições e canais, de uso muito mais simples e mais conáveis que o modelo Java. O pacote CTJ fornece um pequeno conjunto de padrões de projeto que é suciente para programação concorrente em Java. Uma importante vantagem do pacote CTJ é que o programador pode aplicar um conjunto de regras para eliminar os race hazards, deadlock, livelock, starvation, etc. em tempo de projeto e implementação. Pensar sobre o comportamento dos processos é abstrato e cognitivo para programadores, porque a sincronização e escalonamento dos processos foi muito simplicada com a comunicação entre canais e construtores compostos. Tornando mais fácil a depuração e o acompanhamento dos estados do processo. 29 Referências [1] Abhijit Belapurkar. CSP for Java programmers. IBM, June 2005. [2] Carl Hewitt. Viewing control structures as patterns of passing messages. Artif. Intell., 8(3):323364, 1977. [3] Gerald Hilderink, Jan Broenink, Wiek Vervoort, and Andre Bakkers. Communicating Java Threads. In A. Bakkers, editor, Parallel Programming and Java, Proceedings of WoTUG 20, volume 50, pages 4876, University of Twente, Netherlands, 1997. IOS Press, Netherlands.