Sistemas de Computação I
Lista de Exercícios - 18/6/98
No início dos anos 70, foram definidas algumas linguagens de programação que ofereciam o conceito de monitor. Um monitor é análogo a um tipo abstrato de dados, encapsulando dados e operações de acesso. No entanto, as operações de acesso oferecem a garantia de exclusão mútua. Vários processos podem compartilhar acesso a um monitor (e a seus dados), mas no máximo um processo pode estar executando o código dentro do monitor a cada instante. Além da exclusão mútua, monitores oferecem, tipicamente, variáveis de condição e primitivas wait e notify.
Java, que define concorrência baseada em threads, oferece um conceito bastante similar ao de monitores. Métodos de um objeto definidos como synchronized garantem execução com exclusão mútua em relação a qualquer outro método também definido dessa forma. Como exemplo, a definição de classe
public class ExpandableArray { ... // dados public synchronized int size () { ... } public synchronized Object at (int i) {... } public synchronized void append (Object o) { ... } public synchronized void removeLast () { ... } }garante acesso sequencial a qualquer objeto da classe ExpandableArray.
Em contraste com monitores tradicionais, que oferecem variáveis de condição, Java oferece a primitiva de sincronização wait, que faz um processo ficar bloqueado numa fila de espera associada a um objeto, notify, que acorda o primeiro thread nessa fila, e notifyAll, que acorda todos os threads na fila. Não há uma condição específica associada a cada uma dessas primitivas. Observe que, de forma geral, também não há como saber qual thread é o primeiro em uma fila de espera. Por isso, muitas vezes é necessário usar notifyAll, que acorda todos os threads, e fazer com que o bloqueio (wait) seja feito dentro de um loop que testa repetidamente a condição desejada. Como exemplo (bobo) de uso típico, considere a implementação de um buffer limitado:
public class BoundedBufferV implements BoundedBuffer { // dados ... public synchronized void put (Object x) { while (usedSlots == array.length) // espera ate' ter lugar wait(); // coloca no array if (usedSlots++ == 0) // estava vazio notifyAll(); } public synchronized Object take () { while (usedSlots == 0) // espera ate' ter algum item wait(); // retira item x do buffer if (usedSlots-- == array.length) // estava cheio notifyAll(); return x; } }
Usando os mecanismos descritos (não se preocupe com sintaxe, apenas suponha que existem métodos sincronizados e primitivas wait, notify e notifyAll):
Observe que, com o uso de semáforos, é possível sinalizar condições específicas (por exemplo, pote vazio, no caso dos canibais), e que isso não é mais possível com o uso de notify e notifyAll. Discuta as implicações dessa diferença nas soluções apresentadas.
Para relembrar os problemas (sendo que agora não é mais para usar semáforos!)...:
Este problema é uma extensão dos leitores e escritores. Descreva as ações dos processos de busca, inserção e eliminação usando semáforos. Analise sua solução em relação à possibilidade de starvation para cada um dos tipos de processos.