Podemos traduzir as construções (anteriormente definidas) para Java, tendo boa parte delas uma tradução directa. Porém, existem quatro expressões onde a tradução não é linear:fork,wait,sharedesync. As duas primeiras são traduzidas para expressões com fios de execução em Java (da classe java.lang.Thread), enquanto a implementação das primitivasshared e sync envolve a implementação de monitores com auxílio do pacote java.util.concurrent.
Os fios de execução, na linguagem que queremos implementar, denotam um valor. Definimos o interfaceThreadValuecom dois métodos: startejoin. O primeiro cria e inicia um novo fio de execução, e retorna o próprio objecto que o representa. O segundo espera pela conclusão da expressão a executar e retorna o valor computado:
interface ThreadValuehTiextends Runnable { public ThreadValue<T> start();
public T join() throws InterruptedException;
}
Neste contexto, a tradução da primitivaforké feita com base na criação de um objecto, que implementa a interface ThreadValue, em que é encapsulado, no seu método run, a tradução da expressão a avaliar. Depois de criado o objecto, é invocado o métodostart
5. UMALINGUAGEM DEPROGRAMAÇÃOCONCORRENTE 5.2. Tradução para Java
[[forkE]],new ThreadValue<T>() { private T val;
private Thread t;
public void run() { this.val =[[E]]; }
public ThreadValue<T> start() { t = new Thread(this); t.start(); return this; } public T join() throws InterruptedException { t.join(); return this.val; }
}.start();
No caso da expressãowait eé traduzida a sub-expressão e, que deverá corresponder a um fio de execução representado pela interfaceThreadValue, sendo de seguida invocado o método (bloqueante)join, que retorna o resultado calculado pelo fio de execução:
[[waitE]], [[E]].join()
As expressõessharedesyncsão implementadas com base no conceito de Monitor. O próprio Java já oferece suporte para monitores, tais como a primitiva synchronizede os métodos wait, notifye notifyAllda classe Object. No entanto, existem algumas limitações, como por exemplo o facto de não permitir estarem associadas, ao mesmo monitor, duas ou mais filas de espera. Assim, decidimos utilizar o pacote java.util.concurrent que permite associar ao mesmo monitor várias filas de espera. Na figura 5.5 apresen- tamos, parcialmente, a classe de suporte para a implementação dos locks na linguagem (ver anexo 9.1 para código completo). O objecto monitor contém um objecto da classe
ReentrantLock, duas variáveis condição (uma para os leitores e outra para os escritores) associados a esse lock, e um valor inteiro que pode estar dentro de um de três conjuntos para representar o estado do monitor: −1 se está um escritor activo no monitor; 0 se não está nenhum leitor nem nenhum escritor activo no monitor; qualquer valor positivo, in- dicando o número de leitores activos. Todos os métodos do monitor são executados em exclusão mútua, visto que acedem e alteram variáveis partilhadas.
A expressão shared(e1){e2} é traduzida na sequência composta pela avaliação da
expressão que denota o lock, a aquisição do direito de leitura (startShared), a tradução do corpo, seguida da libertação do mesmo lock (endShared):
[[shared(e1){e2}]], {Monitor m= [[e1]]; m.startShared(); [[e2]]; m.endShared();}
Assim, o método startSharedbloqueia o fio de execução se um escritor tiver adquirido, e ainda não tiver libertado, o direito de escrita (contador < 0). O fio de execução pode avaliar a expressão e2 quando garantidamente não houver escritores activos para esse
monitor.
A expressãosync(e1){e2}é traduzida pela sequência composta pela avaliação da ex-
pressão que denota o lock, a aquisição do direito de escrita (startSync), a tradução do corpo, seguida da libertação do mesmo lock (endSync):
5. UMALINGUAGEM DEPROGRAMAÇÃOCONCORRENTE 5.2. Tradução para Java class Monitor {
private ReentrantLock mon; private Condition canRead; private Condition canWrite; private int count;
public Monitor() {...}
public void startShared() throws InterruptedException {...} public void endShared() {...}
public void startSync() throws InterruptedException {...} public void endSync() {...}
}
Figura 5.5: Esquema da classe de suporte (Monitor)
O métodostartSyncadormece o fio de execução se algum escritor, ou algum leitor, estive- rem activos no monitor (contador != 0). O fio de execução só avalia a expressão e2quando
garantidamente é o único activo no monitor.
De salientar que tanto nostartSharedcomo nostartSyncoptámos por seguir uma abor- dagem em que, um fio de execução após ter sido acordado verifica novamente a condição, mesmo que o fio de execução que acorda (o outro) assegura essa mesma condição, isto porque não há garantias, tanto no Java, como no pacote java.util.concurrent, que o fio de execução não possa acordar, não apenas por ter recebido um sinal, mas também porque podem ocorrer spurious wakeups. Este fenómeno deve-se às implementações das operações bloqueantes dos fios de execução, a nível das chamadas ao sistema, de boa parte dos Sistemas de Operação. Podem existir casos em que fio de execução acorda sem que outro fio o tenha acordado. Além deste motivo, tanto no Java como no pacote utili- zado, o métodonotify/signalnão passa o lock do processo que o invocou para o processo que foi acordado. Apenas acorda o fio de execução, tendo este que voltar a adquirir o lock, ao contrário da especificação de Hoare [18]. Assim, o fio de execução que invocou o métodosignalpode ainda executar código dentro do monitor, podendo alterar o estado do monitor de tal forma que o fio de execução que foi acordado, quando voltar a adquirir o lock, encontre o monitor num estado incoerente. Além desta segunda razão, há uma terceira relacionada com o facto de que o fio acordado pode não ser o primeiro a entrar no monitor depois do fio de execução que invocou o métodosignalsair. É possível que um terceiro fio de execução consiga obter o lock antes do fio de execução acordado.
Declarações Como a linguagem Java não suporta declarações aninhadas, recorremos
ao aninhamento de classes/objectos para obter o mesmo efeito. Damos como exemplo a tradução das declarações de variáveis (as declarações de constantes seguem lógica seme- lhante). Utilizamos a seguinte interface para representar uma expressão de declaração de variáveis:
5. UMALINGUAGEM DEPROGRAMAÇÃOCONCORRENTE 5.3. Semântica Operacional