2009-10-30

Variáveis de condição (banco)

O primeiro exercício do terceiro guião pede para reimplementar a classe Banco de modo a bloquear as operações que conduzam a saldos negativos. Como o título é “Variáveis de Condição” o que realmente se pretende é que as operações que conduzam a saldos negativos bloqueiem até que seja possível efectuá-las e não que sejam proibidas, para resolvermos este problema com variáveis de condição em lugar de simples ifs.

Aqui vou assumir que não se fazem transacções de valores negativos (creditar –42, debitar –42, transferir –42) porque, para além de não fazer sentido é uma questão de validação do input e isso não nos interessa para este problema.

Desta forma, a operação de crédito nunca conduz a saldos negativos. Apenas os débitos e as transferências podem criar uma situação deste género. Vamos começar pelos débitos.

Como é que podemos esperar até que o débito possa ser efectuado? Como já devem saber, esperas activas devem ser sempre evitadas em favor de esperas passivas. Uma espera passiva implica que a thread pare a sua execução até a condição de espera deixe de se verificar, enquanto numa espera activa a thread está constantemente a verificar a condição de espera.

Mas, como ainda não conhecemos mecanismos de espera passiva em Java, vamos estudar uma implementação com espera activa para percebermos que funcionalidades necessitamos nesse mecanismo:

public synchronized void debit(int amount) {
while (balance < amount)
/* do nothing */;

balance -= amount;
}

Se pensarem um pouco podem verificar facilmente que esta implementação tem um grave problema. Se a espera começar nunca irá terminar. Como as operações correm em exclusão mútua, nenhuma outra operação nesta conta irá executar, logo o seu saldo nunca será alterado e a condição do ciclo será eternamente verdadeira.

Então é necessário que as threads libertem a lock quando entram em espera. No entanto, não podemos violar a garantia de exclusão mútua, por isso quando a espera terminar, a lock deve ser readquirida antes de prosseguir.

Existe um mecanismo em Java que faz exactamente isto, com esperas passivas: um mecanismo de variável de condição intrínseco a cada objecto.

Na classe Object existem estes três métodos:

void wait() throws InterruptedException;
void notify();
void notifyAll();

Com estão definidos em Object todos os objectos (de todas as classes) têm estes métodos.

  • wait() liberta a lock intrínseca, espera até uma chamada a notify() ou notifyAll() e readquire a lock intrínseca;
  • notify() “acorda” uma das threads que estejam bloqueadas em wait() neste objecto;
  • notifyAll() “acorda” todas as threads que estejam bloqueadas em wait() neste objecto;

Com wait() a nossa espera passa a ser assim:

public synchronized void debit(int amount) throws InterruptedException {
if(balance < amount)
wait();
balance -= amount;
}

Já vou falar da InterruptedException, mas para já ainda falta outro detalhe/máfia…

Uma thread que invoque wait() bloqueia até uma destas condições se verificar:

  • Uma outra thread chama notify() no mesmo objecto e esta é escolhida arbitrariamente para ser acordada;
  • Uma outra thread chama notifyAll() no mesmo objecto;
  • Uma outra thread interrompe esta thread;
  • Ocorre um spurious wakeup;

Vamos ignorar a terceira situação porque não vamos usar o mecanismo de interrupção de threads. Bem, vamos tentar ignorá-la, porque wait() declara que pode lançar InterruptedException… Já disse que detesto checked exceptions?

A última situação é a mais estranha de todas. Basicamente, um spurious wakeup é quando uma thread é acordada por razão nenhuma (o quê???) . Isto é uma situação rara que tem a ver com a implementação da JVM. Mas devemos sempre ter isso em conta e verificar se a condição de espera ainda se verifica depois de wait(). No fundo, devemos esperar sempre em ciclos:

public synchronized void debit(int amount) throws InterruptedException {
while(balance < amount)
wait();
balance -= amount;
}

E agora… a excepção. Aqui a melhor solução é deixar a excepção passar. Se engolissemos a excepção (com try-catch) podiam acontecer coisas más… Imaginem que isto acontece numa transferência. Se ignorarmos a excepção o crédito na outra conta será efectuado na mesma, sem ter ocorrido o débito. Se deixarmos passar a excepção a transferência será interrompida e o crédito nunca terá lugar.

Pois como se isso em Java fosse simples. Agora temos de tratar desta excepção nas transferências. Aqui também não devemos ignorá-la porque não queremos que quem pediu para efectuar a transferência pense que tudo correu bem. Então temos de alterar mais um cabeçalho:

public void transfer(int from, int to, int amount) throws InterruptedException;

Agora passamos o problema para os métodos que chamam este. Já disse que detesto checked exceptions?

Mas voltemos ao que interessa. Agora que temos uma espera com wait() temos de chamar notify() ou notifyAll() algures para que as threads que entrem nessa espera eventualmente acordem.

Quando é que essa thread pode acordar? Quando o saldo da conta aumentar, ou seja, quando for efectuado um crédito:

public synchronized void credit(int amount) {
balance += amount;
notify();
}

Reparem que isto não garante que o débito já seja possível. Se queremos fazer um débito de 100 e só creditamos 10, não chega. No entanto, quando a thread acordar vou testar a condição do ciclo e voltar a esperar.

Uma outra situação é quando o crédito dá para vários débitos que estão em espera. Se tivermos 3 débitos de 10 em espera e creditarmos 30 todos eles podem ser efectuados. O melhor é usar notifyAll() em lugar de notify().

Todas as situações em que podemos usar notify(), notifyAll() também funciona (embora possa ser menos eficiente). No entanto há situações (como esta) em que só notifyAll() resolve o problema. Devem usar sempre notifyAll(), a menos que tenham certeza que notify() chega. notifyAll() funciona sempre.

Temos então de usar notifyAll():

public synchronized void credit(int amount) {
balance += amount;
notifyAll();
}

Para testar utilizei este cenário:

  • Inicializar todas as contas a zero;
  • Existem duas threads:
    • Uma delas percorre todas as contas 10 vezes creditando 100 em cada uma a cada passagem;
    • A outra thread debita 1000 de cada conta;
  • Cada thread conta quanto dinheiro movimentou;
  • Durante a execução as contas não podem ter saldo negativo;
  • No final queremos que ambas tenha movimentado o mesmo;

Convém que sejam muitas contas para que os créditos não sejam todos efectuados antes dos débitos começarem. Não vou reproduzir o código para testar aqui porque não é nada de especial.

0 commentários:

Enviar um comentário