• Nenhum resultado encontrado

Programação concorrente usando threads em Java uma introdução

N/A
N/A
Protected

Academic year: 2021

Share "Programação concorrente usando threads em Java uma introdução"

Copied!
53
0
0

Texto

(1)

Programação Concorrente (CC3037) Departamento de Ciência de Computadores Faculdade de Ciências da Universidade do Porto

Eduardo R. B. Marques, edrdo@dcc.fc.up.pt

Programação concorrente

usando threads em Java

(2)
(3)

Processos

Antes de falarmos em threads, convirá relembrar em

traços gerais o que é um processo:

Um processo é uma instância de execução do código fonte de

um programa.

Ao nível de um sistema operativo, cada processo tem um

espaço de endereçamento de memória independente.

Certos recursos (ex. "file descriptors" para I/O) podem ser

partilhados por processos.

Contudo a interação entre processos tipicamente passa pelo

uso explícito de primitivas de comunicação inter-processos ao

nível do sistema operativo (ex. filas de mensagens, semáforos,

zonas de memória partilhada definidas de forma explícita).

(4)

Processos — organização em memória

stack heap data section text section (code) free memory 0xFF..FF main f1 f2 main f1 f2 PC SP . . . . . . FP arg 1 for f2 arg n for f2 . . . saved FP return address . . . local var 1 local var 2

Secções de memória

“Text” = Código

“Data” = vars. globais / constantes

“ S t a c k ” = p i l h a d e execução; contém “stack frames”, uma por chamada a um procedimento de código na secção “Text”.

“ H e a p ” = m e m ó r i a dinamicamente alocada; gerida automaticamente em linguagens com “garbage collection” ou de forma explícita pelo programador (ex. malloc/free em C)

(5)

Processos com várias threads

Uma thread é uma linha de execução sequencial em um processo. Cada thread tem a seu própria pilha de execução e PC.

Á parte a sua própria pilha de execução, uma thread partilha de forma

transparente as outras zonas de memória (código, heap, ...) com as threads

do mesmo processo bem como outros recursos.

Processo multithreaded = { threads } + { recursos do processo}

Heap Data Text Stack Stack Resources Identity Registers ... var1 var2 start() ... task_one() ... task_two() ... terminate() ... task_one() start() task_two() Files Locks Sockets ... PID UID GID ... SP PC ... ... Registers SP PC ... Thread 1 Thread 2

(6)

Processos com várias threads (cont.)

As threads de um processo executam de forma concorrente.

O escalonamento de threads / processos é tipicamente

preemptivo e opera mudanças de contexto não-deterministas.

A afinidade de processos/threads a “cores”/CPUs varia ao longo

da execução também de forma não determinista.

tempo

CPU

CPU

P1

P2

time

(7)

Implementações do modelo de threads

Há várias implementações do modelo de threads

POSIX threads (pthreads)

Windows Threads Library

Várias linguagens tem suporte built-in (primitivas + bibliotecas

standard), tais como Java ou C#.

Apesar das diferença entre linguagens e da forma das

APIs, o conjunto de primitivas nucleares é relativamente

homogéneo.

O caso de Java:

Uso de “threads” e “object monitors” suportados pela Java

Virtual Machine e pela API base do Java.

API especializada em concorrência dado na “package”

(8)

single-threaded vs multi-threaded

(Imagens: “Locks, Actors, And Stm In Pictures”, Aditya Bhargava)

Processo com uma thread Processo com várias threads

Como iremos ver, em programas multi-threaded devemos ter especial

cuidado com o acesso a memória ou outros recursos (ficheiros,

sockets, …) partilhados por várias threads …

(9)
(10)

Hello world!

Referência: java.lang.Thread java.lang.Runnable

Uma thread é uma instância de java.lang.Thread. Há duas formas básicas de definir uma thread em Java (ver várias variações em HelloWorld.java):

Fornecendo uma instância de java.lang.Runnable como argumento à construção da thread. No exemplo acima isso é feito implicitamente recorrendo a uma expressão lambda.

Ou através de uma subclasse de java.lang.Thread com o método run() redefinido.

Uma thread é iniciada com uma chamada ao método start().

Uma thread pode esperar pelo término de outra com uma chamada ao método join().

Thread t = new Thread(() -> {

System.out.println("Hello from spawned thread"); });

t.start();

System.out.println("Hello from main thread"); t.join();

(11)

Hello world! (cont.)

A chamada a t.join() deixa a thread “main” suspensa

enquanto t não termina.

Thread t = new Thread(() -> {

System.out.println("Hello from spawned thread"); });

t.start();

System.out.println("Hello from main thread"); t.join();

(12)

Hello world! (cont.)

System.out.print("How many threads? "); Thread[] t = new Thread[in.nextInt()]; for (int i = 0; i < t.length; i++) { int id = i + 1;

t[i] = new Thread(() -> {

System.out.println("Hello from spawned thread " + id); });

t[i].start(); }

System.out.println("Hello from main thread"); for (int i = 0; i < t.length; i++) {

t[i].join(); }

How many threads? 4

Hello from spawned thread 2 Hello from main thread

Hello from spawned thread 3 Hello from spawned thread 1 Hello from spawned thread 4

How many threads? 4

Hello from spawned thread 1 Hello from spawned thread 2 Hello from main thread

Hello from spawned thread 3 Hello from spawned thread 4

As acções de várias threads concorrentes são intercaladas de forma não determinista.

(13)

“Sono” e interrupções …

Uma chamada a Thread.sleep() leva a que a execução da thread em contexto (isto é, a que fez a chamada) seja suspensa por um período do tempo especificado. Se outra thread chamar t.interrupt() para a instância suspensa, então t é acordada com uma excepção do tipo InterruptedException. Uma thread suspensa numa chamada a join() ou outras operações bloqueantes pode também ser interrompida da mesma forma.

O uso de sleep() e/ou interrupt() não é de forma geral adequado à sincronização entre threads, tipicamente habilitam “hacks de recurso” pouco fiáveis. No exemplo simples em questão temos um comportamento não

determinista: às vezes t é acordada, outras vezes não ….

Thread t = new Thread( () -> { try { System.out.println("going to sleep ..."); Thread.sleep(1000); System.out.println("awake ..."); } catch (InterruptedException e) { System.out.println("got interrupted!"); } }); t.start(); Thread.sleep(999); t.interrupt(); t.join(); going to sleep ... got interrupted! going to sleep ... awake … Execuções possíveis

(14)

Outros métodos em java.lang.Thread

Thread.currentThread() — método estático que evolve a thread em contexto

(a que chama o método).

stop()

Termina abruptamente uma thread.

Tal como sleep() e interrupt() o seu uso não é aconselhado para uma programação “boa” / “normal”.

getName() / setName(): permitem atribuir ou obter o nome da thread;

setDaemon() : permite tornar uma thread como “daemon” - JVM não espera pelo

término de “daemon” threads.

getState() — obtém estado de uma thread que poderá ser um de: NEW — criada mas não iniciada;

RUNNABLE — a executar; TERMINATED — terminada;

BLOCKED — bloqueada na aquisição de um “lock”, veremos depois o que

isso significa;

e WAITING ou TIMED WAITING — bloqueada à espera de uma acção por outra thread (ex. via Thread.join()).

(15)

Partilha de memória

e condições de corrida

(16)

Partilha de memória

class Counter { int value = 0;

void increment() { value ++; }

int getValue() { return value; } }

int tmp = value; // Leitura value = tmp + 1; // Escrita t1 v leitura v+1 escrita !!! t2 v leitura v+1 escrita

(Imagem: “Locks, Actors, And Stm In Pictures”, Aditya Bhargava)

Exemplo: contador usado por várias threads.

Problema: incremento de value não é atómico, pois decompõe-se em: 1) leitura de valor anterior; 2) escrita do valor actualizado.

(17)

Partilha de memória (cont.)

class Counter { int value = 0;

void increment() { value ++; }

int getValue() { return value; } }

int tmp = value; // Leitura value = tmp + 1; // Escrita t1 v leitura v+1 escrita !!! t2 v leitura v+1 escrita

(Imagem: “Locks, Actors, And Stm In Pictures”, Aditya Bhargava)

Há uma condição de corrida (“race condition”) sobre value: várias

operações simultâneas de leitura/escrita sobre o mesmo item de memória partilhada em que pelo menos um das operações é uma escrita.

O incremento do valor é um exemplo de uma secção crítica, i.e. uma região de código onde no máximo uma thread deverá estar activa - devemos

(18)

Partilha de memória (cont.)

void increment() { value ++;

}

2: getfield #15 // Field value:I 5: iconst_1

6: iadd

7: putfield #15 // Field value:I JVM bytecode Java javac t1 v getField v+1 putField !!! t2 v getField v+1 putField

Ao nível da JVM as instruções getField e putField

correspondem à leitura e escrita de um campo de um objecto.

(19)

Partilha de memória (cont.)

static void test(Counter c, int n, int k) throws InterruptedException { System.out.println("=> Testing " + c.getClass().getName());

Thread[] t = new Thread[n]; for (int i = 0; i < n; i++) { t[i] = new Thread(() -> {

for (int j = 0; j < k; j++) c.increment(); });

t[i].start(); }

for (int i = 0 ; i < n; i++) { t[i].join();

}

System.out.printf("expected counter value: %d%n", n*k);

System.out.printf("actual counter value : %d%n", c.getValue()); }

=> Testing BuggyCounter

expected counter value: 2000 actual counter value : 2000

=> Testing BuggyCounter

expected counter value: 2000

actual counter value : 1640

=> Testing BuggyCounter

expected counter value: 2000 actual counter value : 1836

java CounterExample 4 500

Execução não é determinista e o resultado é por vezes incorrectos.

Em certas configurações o “bug” pode passar despercebido. Experimente por exemplo várias execuções do código com 2 threads e 10 incrementos por thread…

(20)
(21)

Blocos synchronized

class Counter { int value = 0; void increment() { synchronized(this) { value ++; } }

Cada objecto Java tem associado um monitor.

Para um bloco synchronized(obj) { … instruções … }

1. Uma thread adquire primeiro o lock sobre o monitor de obj (this no

exemplo) — em correspondência é executada a instrução monitorenter sobre obj na JVM. A execução bloqueia se o lock for detido por outra

thread, e só é retomada depois de adquirido o lock.

2. Após a aquisição do lock, é executado o código dentro do bloco synchronized.

3. É libertado no final o lock sobre o monitor de obj — em correspondência é

executada instrução monitorexit na JVM. Outra thread (apenas uma)

bloqueada no mesmo monitor poderá agora adquirir o lock.

. . .

6: monitorenter 7: aload_0

8: dup

9: getfield #17 // Field value:I 12: iconst_1

13: iadd

14: putfield #17 // Field value:I 17: aload_1

18: monitorexit

(22)

Uso de synchronized (cont.)

t1 v leitura v+1 escrita monitorenter monitorexit t2 v+1 leitura v+2 escrita monitorenter monitorexit bloqueia

(23)

Uso de synchronized (cont.)

Um método de instância com o modificador synchronized tem implícita a aquisição de um lock sobre this à entrada do método a sua libertação no retorno.

As duas implementações acima são equivalentes.

void increment() {

synchronized (this) { value ++;

} }

synchronized void increment() { value ++; } . . . 6: monitorenter 7: aload_0 8: dup

9: getfield #17 // Field value:I 12: iconst_1

13: iadd

14: putfield #17 // Field value:I 17: aload_1

18: monitorexit

(24)

Uso de synchronized (cont.)

Recorrendo a synchronized o contador tem um comportamento correcto e determinista.

Em getValue() deveremos também empregar synchronized para garantir que obtemos garantidamente o valor mais recente de value. Veremos depois porquê …

class Counter { int value = 0;

synchronized void increment() { value ++; }

synchronized int getValue() { return value; } }

=> Testing CorrectCounter

expected counter value: 2000 actual counter value : 2000

(25)

ReentrantLock

Podemos usar também java.util.concurrent.locks.ReentrantLock Operações de aquisição e libertação do “lock” são invocadas explicitamente no código fonte e sem validação de âmbito sintáctico em tempo de compilação (ao contrário de blocos synchronized por definição).

O uso de ReentrantLock é conveniente para um controlo mais “fino” sobre um monitor; em particular suporta múltiplas variáveis de condição em associação a um monitor, conceito que discutiremos mais tarde.

Boa prática: a chamada a unlock deve residir num bloco finally em

associação à região crítica, garantindo que é libertado o “lock” em qualquer caso, mesmo quando for lançada uma excepção dentro da região crítica. Em contraste, blocos synchronized garantem de raíz esse comportamento.

synchronized void increment() { value ++; } ReentrantLock rl = new ReentrantLock(); void increment() { rl.lock(); try { value ++; } finally { rl.unlock(); } }

(26)

ReentrantReadWriteLock

ReentrantReadWriteLock permite por sua vez distinguir locks de escrita e locks de leitura.

Lock de escrita: apenas uma thread pode deter o lock de escrita a data altura. Outras threads à espera de obter o lock de leitura ou de escrita serão bloqueadas.

Lock de leitura: pode ser detido por várias threads simultaneamente, desde que não haja um lock de escrita. Outras threads à espera de obter o lock de escrita serão bloqueadas, mas não threads que queiram obter

ReentrantReadWriteLock rrwl

= new ReentrantReadWriteLock(); void increment() {

rrwl.writeLock().lock(); try { value ++; }

finally { rrwl.writeLock().unlock(); } }

void getValue() {

rrwl.readLock().lock(); try { return value; }

finally { rrwl.readLock().unlock(); } }

(27)
(28)

Uso de primitivas atómicas

A classe AtomicInteger e outras em java.util.concurrent.atomic têm métodos especializados que permitem conduzir o equivalente a 2 operações num só passo atómico.

compareAndSet é uma das primitiva bases que serve para implementar outras,

por exemplo getAndIncrement como se pode ver aqui (para o OpenJDK 7).

Nas implementações acima, ao contrário das que vimos baseadas em locks, está subjacente uma espera activa o que poderá não ser o ideal … falaremos depois private final AtomicInteger value = new AtomicInteger(0);

public void increment() { int v;

do {

v = value.get();

} while(! value.compareAndSet(v, v + 1)); }

public int getValue() { return value.get();

} private final AtomicInteger value = new AtomicInteger(0); public void increment() {

value.getAndIncrement() }

public int getValue() { return value.get(); }

(29)

Exemplos de primitivas atómicas

AtomicInteger ai = …; ar.compareAndSet(testValue, newValue); if (ao.value == oldValue) {  ao.value = newValue; return true; } else { return false; } AtomicInteger ai = …; ai.getAndIncrement();

int oldValue = ai.value; ai.value++;

return oldValue;

AtomicInteger ai = …; ai.getAndSet(newValue)

int oldValue = ab.value; ab.value = newValue;

return oldValue;

Operação

Efeito atómico

(30)

Uso de notificações em monitores

e de variáveis de condição

(31)

Sincronização usando notificações

Além do suporte de “locks” via monitores em blocos synchronized, em Java temos métodos baseados em notificações sobre monitores definidas na classe java.lang.Object .

obj.wait() : leva a que a thread em contexto bloqueie à espera de uma

notificação sobre o monitor de obj - tipicamente existe uma “condição de espera” associada.

obj.notify() e obj.notifyAll() : permitem entregar notificações a

threads à espera de uma notificação em obj .

Estes métodos podem servir para coordenação entre threads, como veremos através de um exemplo. synchronized (obj) { while (! someCondition()) { obj.wait(); } } synchronized (obj) { obj.notify(); } synchronized (obj) { obj.notifyAll(); } entrega de notificações espera

(32)

Exemplo

Fila com capacidade limitada, implementando usando internamente a estratégia de “array circular” e usual disciplina FIFO (“first-in, first-out”):

add() deve bloquear enquanto fila estiver cheia remove() deve bloquear enquanto fila está vazia public class BoundedQueue<E> {

private final E[] elems; private int size;

private int head;

public synchronized void add(E elem) throws InterruptedException {

}

public synchronized E remove() throws InterruptedException {

}

(33)

Exemplo (cont.)

public class BoundedQueue<E> {

public synchronized void add(E elem) throws InterruptedException {

while (size == elems.length) wait(); // FULL !

elems[(head + size) % elems.length] = elem; size++;

notifyAll(); }

public synchronized E remove() throws InterruptedException {

while (size == 0) wait(); // EMPTY !!

E elem = elems[head];

head = (head + 1) % elems.length; size--;

notifyAll(); return elem; }

(34)

Funcionamento em mais detalhe

Para qualquer um dos 3 métodos, a thread em contexto (a que chama o método) deve ter o “lock” sobre obj adquirido.

IllegalMonitorStateException é lançada caso contrário.

obj.wait() — execução padrão para a thread em contexto (há casos

especiais discutidos a seguir):

1. Liberta o “lock” sobre obj e bloqueia. A thread é adicionada ao

conjunto de espera — “wait-set” — de obj.

2. Aguarda envio de notificação associada a obj.

3. Depois de recebida a notificação, o “lock” tem de ser re-adquirido

antes de fi nalmente wait() retornar.

obj.notify() e obj.notifyAll()

notify(): entrega notificação sobre obj a apenas uma das threads no

“wait-set” de obj — a escolha é não-determinista.

notifyAll(): entrega notificação sobre obj a todas as threads no

(35)

Padrão de espera com wait()

Observe que temos um while e não um if … o padrão de espera com wait deve ser do tipo while (condition) { obj.wait(); }

Há duas razões para isso:

1. A recepção de uma notificação pode ser aguardada por várias threads com condições de espera distintas. Cada thread deve portanto verifi car que a sua condição de espera se verifi ca ou não após cada retorno de wait().

2. Existe a possibilidade de “notificações espúrias”, isto é, wait() pode retornar “espontaneamente” sem que tenha havido qualquer notificação! O evento é raro mas possível. Outras implementações de monitores em bibliotecas de threads exibem este comportamento (ex. pthreads, Windows threads).

synchronized (obj) { while (! someCondition()) { obj.wait(); } }

(36)

wait() - aspectos complementares

Analogamente a Thread.join() ou

Thread.sleep() , a e s p e r a e m Object.wait() pode terminar quando uma interrupção é enviada à thread em contexto. Nesse caso, a thread em espera tem de re-adquirir o lock, após o que wait() termina com o l a n ç a m e n t o d a e x c e p ç ã o InterruptedException. synchronized (obj) { while (! someCondition()) { obj.wait(); } } interrupção InterruptedException synchronized (obj) { while (! someCondition()) { obj.wait(1000); } }

O tempo de espera por uma notificação pode ser limitado usando wait(long timeout). Se uma notificação não for r e c e b i d a n o l i m i t e d e t e m p o e s p e c i fi c a d o , wait() r e t o r n a normalmente.

espera com tempo limitado

(37)

Voltando ao exemplo da fila …

Será correcto usar notify() em vez de notifyAll() em add() ou remove() ? Tem a vantagem de potencialmente não acordarmos threads desnecessariamente. Com uma análise cuidadosa podemos concluir que sim:

Uma notificação enviada em add() só poderá ser recebida por uma thread em espera em remove() e vice-versa, mesmo para o caso de uma fila com capacidade 1.

Além disso só uma thread das que estejam em espera em add() ou

remove() poderão progredir (i.e. não voltar a bloquear em wait() ).

Para prevenir erros subtis — e porque a análise não é trivial em muitos casos — o uso notifyAll() é mais seguro.

public synchronized void add(E elem) throws InterruptedException {

while (size == elems.length) wait(); // FULL !

notify(); }

public synchronized E remove() throws InterruptedException {

while (size == 0) wait(); // EMPTY !!

notify();

return elem; }

(38)

Exemplo da fila — variante

import java.util.concurrent.locks.Condition;

import java.util.concurrent.locks.ReentrantLock; public class BlockingQueue2<E> {

private final ReentrantLock qlock = new ReentrantLock(); private final Condition notEmpty = qlock.newCondition(); private final Condition notFull = qlock.newCondition();

Se empregarmos ReentrantLock a fila pode na mesma ter apenas um “lock” associado. Podemos no entanto distinguir as condições de fila cheia e vazia explicitamente através de variáveis de condição (Condition).

Condition define métodos:

await() com funcionamento análogo a Object.wait()

signal() e signalAll() com funcionamento análogo a

(39)

Exemplo da fila — variante (cont.)

public void add(E elem)

throws InterruptedException { qlock.lock();

try {

while (size == elems.length) { notFull.await(); } notEmpty.signal(); } finally { qlock.unlock(); } }

public void remove(E elem) throws InterruptedException { qlock.lock(); try { while (size == 0) { notEmpty.await(); } notFull.signal(); } finally { qlock.unlock(); } }

O código fica mais “arrumado” e a sua análise fica facilitada …

Mas … precisamos sempre de entregar notifi cações ou apenas quando a

fi la “deixa de estar vazia” ou “deixa de estar cheia” ? Por exemplo será correcto sentido ter em add() o seguinte:

if (size == 1)

(40)

Exemplo da fila — variante (cont.)

t1 remove() t2 remove() t3 notEmpty .signal() notEmpty.await() notEmpty.await() add(x) size t4 add(y) 0 1 2 Espera eternamente! NOTIFICAÇÃO “PERDIDA”!

Introduzimos um bug, levando a execuções com um problema conhecido como “lost

wakeup”. Note que acima t4 pode adquirir o lock antes de t2 o readquirir via await().

Comportamento erróneo e não é reproduzível sistematicamente e poderá ser díficil de observar.

notEmpty .signal()

if (size == 1)

(41)
(42)

Deadlock

O exemplo anterior do “lost wakeup” é uma instância dum

problema geral chamado “deadlock”.

Um “deadlock” é caracterizado pelo bloqueio eterno de

uma ou mais threads.

Exemplos de causas de deadlock:

Thread tenta adquirir um “lock” que nunca é libertado por outra

thread.

Thread espera por uma notificação de uma outra thread que

nunca chega ser entregue.

(43)

Deadlock — exemplos simples (1)

Primeira thread não chama unlock() por alguma razão. Segunda thread não consegue portanto adquirir o lock.

rl.lock() . . . rl.unlock() rl.lock() ... rl.unlock() t1 t2 t1 rl.lock() t2 bloqueada rl.lock() ReentrantLock rl;

(44)

Deadlock — exemplos simples (2)

Neste caso 2 threads esperam pelo término uma da outra.

. . . Thread.join(t2); ... Thread.join(t1) t1 t2 t1 Thread.join(t2) t2 bloqueada Thread.join(t1) bloqueada

(45)

Deadlock — exemplos simples (3)

Neste caso: as 2 threads bloqueiam-se mutuamente porque cada uma detém um lock que a outra precisa de obter para progredir na sua execução.

synchronized (a) { synchronized(b) { // . . . } } synchronized (b) { synchronized(a) { // . . . } } t1 t2 t1 obtém lock sobre a

não consegue obter lock sobre b

t2

obtém lock sobre b

não consegue obter lock sobre a

bloqueada

(46)

Jantar dos filósofos

O exemplo anterior generalizado a N threads corresponde a um problema clássico conhecido como o jantar dos Filósofos, “alegoria” sobre a dificuldade

de sincronização em processos concorrentes e a possibilidade de deadlock.

N filósofos [threads ou processos] sentam-se à volta de uma mesa redonda,

passando o tempo a pensar ou a comer esparguete. Um garfo [recurso] é colocado entre cada filósofo, num total de N garfos. Antes de começar a comer, um filósofo tem de pegar no garfo à sua esquerda e depois no garfo à sua direita. Se todos os filósofos pegarem no garfo à sua esquerda ao mesmo tempo, nenhum conseguirá pegar no garfo à sua direita [deadlock] e

class Philosopher extends Thread { Fork leftFork;

Fork rightFork;

public void run() {

synchronized(leftFork) { synchronized(rightFork) { // eat ... } } } }

(47)

Exemplo — “Contagem decrescente”

Acima: Uma implementação (correcta) para um “count-down latch” — em

português, “trinco de contagem decrescente” — com funcionalidade parcialmente similar à classe com o mesmo nome em java.util.concurrent.

await(): aguarda que contagem chegue a 0;

countDown(): decrementa contagem se ainda não for 0, notifi ca threads

em espera quando contagem chega a 0;

class CountDownLatch { private int value; ...

synchronized void await() throws InterruptedException { while (value > 0) wait();

}

synchronized void countDown() {

if (value > 0 && --value == 0) notifyAll(); }

(48)

“Contagem decrescente” — ilustração

synchronized void await() throws InterruptedException { while (value > 0) wait();

}

synchronized void countDown() {

if (value > 0 && --value == 0) notifyAll(); }

await

(49)

Implementação com deadlock (1)

synchronized void await() throws InterruptedException { while (value > 0) wait();

}

synchronized void countDown() {

if (value > 0 && --value == 0) notify();

} await() countDown() 3 2 1 0 value U s a n d o notify() em v e z d e notifyAll() a p e n a s u m a das threads em espera acorda, a s o u t r a s e n t r a m e m deadlock!

(50)

Implementação com deadlock (2)

synchronized void await() throws InterruptedException { while (value > 0) wait();

}

synchronized void countDown() { if (value > 0 && --value == 0)

synchronized(this) { notifyAll() } } await() countDown() Decremento de value não é sincronizado e p o r t a n t o s u j e i t o a condições de corrida. Secção crítica não executa atomicamente.

De forma análoga ao exemplo do contador, contador pode ficar com v a l o r i n c o r r e c t o ( p o d e m o s “ p e r d e r ” actualizações).

Deadlock: nenhuma das

t h r e a d s e m e s p e r a acorda

(51)

Implementação com deadlock (3)

synchronized void await() throws InterruptedException { while (value > 0) synchronized(this) { wait(); }

}

synchronized void countDown() {

if (value > 0 && --value == 0) notifyAll(); }

Teste sobre value não é sincronizado e portanto sujeito a

c o n d i ç õ e s d e c o r r i d a . S e c ç ã o crítica não executa atomicamente.

Uma das threads p o d e l e r v a l o r positivo e depois o valor já ser 0 quando entra em espera. A thread em causa fica em deadlock.

await()

countDown()

3 2 1 0

(52)

Implementação com deadlock (4)

synchronized void await() throws InterruptedException { while (true) { boolean b;

synchronized (this) { b = (value > 0); }

synchronized (this) { if (b) wait(); else break;} } } Não há condições de corrida mas s e c ç ã o c r í t i c a continua a não e x e c u t a r atomicamente. “Sincronização em 2 p a s s o s ” p o d e levar a situação análoga ao caso anterior. await() countDown()

(53)

Violação de atomicidade

Os últimos 3 exemplos ilustram o problema de violação

de atomicidade.

Temos uma violação de atomicidade quando uma secção

crítica não executa em um único passo atómico que é

livre da interferência de outras threads.

synchronized void await() throws InterruptedException { while (true) { boolean b;

synchronized (this) { b = (value > 0); }

synchronized (this) { if (b) wait(); else break;} }

}

synchronized void await() throws InterruptedException { while (value > 0) synchronized { wait(); }

}

synchronized void countDown() { if (value > 0 && --value == 0) synchronized { notifyAll() }

Referências

Documentos relacionados

Esta foi uma reflexão que, pela oportunidade, pela colaboração, pela partilha e essencialmente pelo exercício de criatividade que foi necessário fazer face às

Serão realizadas também aulas remotas com turmas pequenas (decidir com a turma) para discussão entre o professor e os alunos referentes a cada ítem da ementa de laboratórios

Dos resultados, sublinha-se: o início mais jovem na forma medular comparativamente à forma bulbar, e a predominância do sexo feminino neste último tipo de apresentação; o

Para instalar o porteiro AM-PPR com apenas uma tecla reservada para central AM-C100 é necessário colocar o “JUMPER” J1 e retirar o “JUMPER” J2 existente na placa

(NBR 5419:1997) 6.4.2.4.1 Em qualquer instalação deve ser previsto um terminal ou barra de aterramento principal e os seguintes condutores devem ser a ele ligados: Condutores

Além disso, a rotativade voluntária das empresas premiadas no GPTW 50+ é muito mais baixas e as empresas são mais diversas em outros pilares também.. Ranking GPTW - Melhores

O PROGRAMA DE PÓS-GRADUAÇÃO EM FISIOPATOLOGIA E CIÊNCIAS CIRÚRGICAS, DA UNIVERSIDADE DO ESTADO DO RIO DE JANEIRO - UERJ torna público o presente Edital,

Indicada para o assentamento diferenciado de revestimentos cerâmicos, grês (fachadas até 6 pavimentos/peças até 60 x 60 cm), porcelanato técnico/esmaltado (fachadas até