Programação Concorrente Segura em Java

Propaganda
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.
Download