Você está na página 1de 53

Computação Paralela e

Distribuída

Prof. Marco Túlio Valente


mtov@pucminas.br
Programa do Curso

 Parte I: Programação Multithread/Paralela


– Arquiteturas, Conceitos e Características, [1]
– Implementação de Seções Críticas [1]
– Semáforos e Monitores [1]
– Estudo de Caso: Java [4]
 Parte II: Programação Distribuída
– Conceitos e Características [1,2,3]
– Middlewares [3]
– Estudo de Caso: Java RMI [3] e CORBA [3]
– Algoritmos Distribuídos [2]
 Objetivo: conciliar teoria (fundamentos) tecnologia
(ferramentas) e prática (problemas)
 Bibliografia:
[1] Andrews, G. Foundations of Multithreaded, Parallel, and
Distributed Programming, Addison-Wesley, 2000.
[2] Couloris, G., Dollimore, J., Kindberg, T. Distributed
Systems Concepts and Design (Third Edition). Addison-
Wesley, 2001.
[3] Emmerich, W. Engineering Distributed Objects. John
Wiley & Sons, 2000.
[4] Arnold, K. et al. The Java Programming Language,
Third Edition, Addison-Wesley, 2000.

® MTOV 2
Programação Concorrente (cap. 1)

 Programas seqüenciais: único fluxo execução


 Programa concorrentes:vários fluxos execução
– Multithread, Paralelos ou Distribuídos
 Multithread:
– núm. fluxos execução > núm. processadores
– Objetivo: “fazer várias coisas, mesmo tempo”
– Ex.:GUI, SO, SGBD, servidores Web etc
 Paralelos:
– núm. fluxos execução = núm. processadores
– Objetivo: “fazer uma tarefa mais rápido”
– Ex.: multiplicar matrizes, previsão tempo,
processar/gerar imagem, simulações etc
 Distribuídos:
– fluxos de execução em nodos de uma rede
– Objetivo: “compartilhar recursos”
– Ex.:aplicações C/S (mail, web, dados etc)
 Comunicação/Sincronização:
– Mem. compartilhada: multithreads, paralelos
– Troca de mensagens: todos os 3 tipos

® MTOV 3
Programação Concorrente

 Origens: canais nos primeiros SO (década 60)


– Canais: processadores de E/S
– Exemplo: SO grava dados em um buffer; canal
transfere do buffer para disco
– SO deve esperar buffer vazio; canal deve
esperar buffer cheio
 Atualmente, cada vez mais comum:
– GUI, Internet, Web, C/S arq. paralelas etc
 Programação concorrente é mais desafiadora que
programação seqüencial
 Principal dificuldade: sincronização
 Exclusão Mútua: assegurar que seções críticas
não sejam executadas ao mesmo tempo
– Dois processos não podem gravar ao mesmo
tempo no buffer
 Sincronização condicional: atrasar a execução até
que uma condição seja verdadeira
– Canal só pode ler dados se buffer cheio

® MTOV 4
Conceitos e Arquiteturas

 Fluxos execução assíncronos: cada fluxo de


execução tem a sua própria “velocidade”
 Fluxo de execução: pilha (variáveis locais) + PC
 Processo: fluxo execução + espaço de
endereçamento próprio
 Thread (“processos leves”): fluxo execução +
espaço de endereçamento compartilhado
 Arquiteturas Monoprocessadas:
– Programação seqüencial com interrupções
– Núcleo de multiprogramação (SO, VM, ou
biblioteca) cria um modelo de programação
assíncrono
– Vários processos compartilham CPU
 Arquiteturas Multiprocessadas:
– Memória Compartilhada
– Memória Distribuída

® MTOV 5
Multiprocessadores com
Memória Compartilhada

 Podem ser:
– UMA (Uniform Memory Access Time)
– NUMA (Non-Uniform Memory Access Time)
 Exemplo (CENAPAD): Starfire ENT1000 (Sun), 32
processadores, 1MB cache, 8GB RAM
 Comunicação: via memória compartilhada ou troca
de mensagens
 Vantagem: barramento de alta velocidade,
memória compartilhada facilita programação
 Desvantagem: limitação no número de nodos

® MTOV 6
Multiprocessadores com
Memória Distribuída

 Processos podem acessar apenas memória local


 Processos se comunicam por troca de msgs
 Exemplo (CENAPAD): IBM RS/6000, 48
processadores
 Vantagem: escalabilidade
 Desvantagem: dificuldade de programação
 Memória Compartilhada Distribuída (DSM):
abstração de um espaço de endereçamento único
criada pelo SO
– SO mapeia “endereço virtual” para endereço
físico de um processador
– Desvantagem: custo de comunicação

® MTOV 7
Clusters

 Computadores “comuns” conectados em rede


 Cada nodo tem sua CPU, memória e SO
 Vantagens: escalabilidade, custo, tolerância a
falhas
 Desvantagens:conexão em rede e administração
– Custo de administração similar ao de n
máquinas independentes
 Exemplo: Google
– Vários clusters, distribuídos no mundo
– 15 mil PCs (de Celeron a Dual Pentium III)
– Sistema operacional: Linux
– PCs possuem discos IDE de 80 GB
– Objetivo: performance + hardware barato
– Ver: Web Search for a Planet: The Google
Cluster Architecture, IEEE Micro, March 2003

® MTOV 8
Multiplicação Matrizes
(Memória Compartilhada)
 Multiprocessadores com memória compartilhada
 Idéia: calcular produtos internos paralelamente
 Variáveis compartilhadas:
double a[n,n], b[n,n], c[n,n]
 Código da multiplicação:
process row [i= 0 to n-1] % cria n processos
for j:= 1 to n-1 do
c[i,j]= 0;
for k:= 1 to n-1 do
c[i,j] = c[i,j] + a[i,k] * b[k,j]
 Na solução acima, cada processo gera uma linha
da resposta
 Outra solução: computar cada produto interno em
paralelo
process [i= 0 to n-1, j= 0 to n-1] % cria n*n proc
c[i,j]= 0;
for k:= 1 to n-1 do
c[i,j] = c[i,j] + a[i,k] * b[k,j]

® MTOV 9
Multiplicação de Matrizes
(Memória Distribuída)
process worker[i=0 to n-1] {
double a[n]; double b[n,n]; double c[n];
receive initial values for vector a and matrix b;
for [j=0 to n-1] {
c[j]=0.0;
for [k=0 to n-1]
c[j]=c[j] + a[k]*b[k,j];
}
send result vector c row to the coordinator process;
}
process coordinator {
double a[n,n]; double b[n,n]; double c[n,n];
initialize a and b;
for [i=0 to n-1] {
send row i of a to worker[i];
send all of b to worker[i];
}
for [i=0 to n-1]
receive row i of c from worker[i];
print the result matrix c;
}

® MTOV 10
IPC via Unix Pipes

 Pipes: mecanismo de IPC (Inter Process Comm.)


 Fila de bytes unidirecional residente no kernel
 Criação de um pipe:
int d [2];
int pipe (d);
 Escrita e leitura
int write (d[1], “hello world”);
int read (d[0], buffer, tam_buffer);
 Pipes entre processos diferentes:
– Criado por processo pai; em seguida, pai
executa um fork
– Pai e filho passam a compartilhar o pipe
 Somente pode ser usado entre processos pai e
filho ou entre “processo irmãos”
 Se necessário fluxo bidirecional: dois pipes
 Comando Unix: who | sort | lpr
 Outros mecanismos IPC em Unix: FIFO, Fila de
Mensagens, Memória compartilhada

® MTOV 11
Produtores/Consumidores via
Pipes
void main () { // comunicação de pai para filho
int n, d[2];
pid_t pid;
char line [MAX_LINE];
if (pipe (d) < 0)
printf (“Erro na criação do pipe”);
else ((pid = fork()) < 0)
printf (“Erro no fork”);
else if (pid > 0) { // processo pai: pid= PID filho
close (d[0]);
write (d[1], “hello world\n”);
}
else { // processo filho: pid = 0
close (d[1]);
n= read (d[0], line, MAX_LINE); // síncrona
write (1, line, n); // 1 = stdout
}

® MTOV 12
Processos e Sincronização (cap. 2)

 Estado: valor das variáveis de um processo em


um instante (incluindo PC, stack etc)
 Processo executa uma seqüência de comandos
 Comando: sequëncia de ações atômicas
 Ação atômica:ação que inspeciona/altera estado
de forma indivisível
– Exemplos: instruções de máquina que acessam
palavras da memória
 Execução de programa concorrente gera uma
história: s0  s1  ...  sn (si: ação atômica)
 Histórias podem variar de execução para outra
 Exemplo (atribuições e leituras atômicas):
int y= 0, z= 0;
P1:: x = y +z; P2:: y= 1; z= 2;
 Algumas histórias possíveis
– rd y  rd z  x= y+z  y= 1  z= 2 x= 0
– y= 1 rd y  rd z  x= y+z  z= 2 x= 1
– rd y  y= 1  z= 2  rd z  x= y+z x= 2
– y= 1  z= 2  rd y  rd z  x= y+z x= 3

® MTOV 13
Sincronização

 Sincronização: restringe as histórias possíveis


 Exclusão mútua: requer a criação de ações
atômicas via software
– Uma ação atômica ”de maior granularidade” é
chamada de seção crítica
 Exemplo: < ... > cria uma seção crítica
int y= 0, z= 0;
P1:: <x = y +z;> P2:: <y= 1; z= 2;>
 Histórias possíveis:
– rd y ; rd z ; x= y+z  y= 1 ; z= 2 x= 0
– y= 1 ; z= 2  rd y ; rd z ; x= y+z x= 3
 Sincronização condicional: atrasar uma ação
atômica até que uma condição seja verdadeira
– Até que o estado atenda uma condição
 Propriedade: condição que é verdadeira em
qualquer história de um programa concorrente
 Podem ser de dois tipos: safety e liveness

® MTOV 14
Safety e Liveness

 Safety: propriedades que asseguram a não


ocorrência de um estado indesejável
 Liveness: propriedade que assegura a ocorrência
de um estado desejável
 Dado um programa concorrente mostrar:
– Não ocorre deadlock/livelock (safety)
– Possui uma seção crítica (SC) (safety)
– Termina (liveness)
– Eventualmente entra em uma SC (liveness)
 Como provar que uma propriedade é atendida?
– Testes (“provam a presença de erros, mas não
a ausência”)
– Formalmente (via asserções etc)
 Notação (similar a linguagem SR):
co “bloco de comandos”
co “bloco d comandos” // em SR, “// “
oc
 “Braços de um comando co” são executados em
paralelo (comando termina quando todos braços
terminarem)

® MTOV 15
Independência de Processos
Concorrentes
 read set: variáveis lidas e não alteradas
 write set: variáveis que são alteradas
 Independência: dois blocos de comandos são
independentes se o “write set” de cada bloco é
disjunto do “read set” e “write set” do outro bloco
 Exemplo:
– P1:: x = y +z; P2:: y= 1; z= 2;
read_set(P1)= {y, z} write_set(P2)= {y, z}
 race condition: quando programa permite a
execução concorrente de blocos de comandos não
independentes (ou conflitantes)
 Exemplo: como paralelizar um grep?
String line;
read a line of input from stdin into line;
while (!EOF) {
look for pattern in line;
if (pattern is in line)
write line;
read next line of input;
}

® MTOV 16
Exemplo: grep paralelo

 Versão incorreta (com conflitos):


String line1;
read a line of input from stdin into line1;
while (!EOF) {
co look for pattern in line1; // read set= line1
if (pattern is in line1) print line1
co read next line into line1; // write set= line1
oc
}
 Versão correta:
String line1,line2;
read a line of input from stdin into line1;
while (!EOF) {
co look for pattern in line1; // read set= line1
if (pattern is in line1) print line1
co read next line into line2; // write set= line2
oc
line1 = line2;
}
 Desvantagens: cópia entre linhas e criação de
duas threads a cada repetição do laço

® MTOV 17
Sincronização

 Execução de programa concorrente: intercalação


de “ações atômicas”
 Sincronização: evita “intercalações indesejadas”
– Via seções críticas
– Via sincronização condicional
 Ação atômica sem condição (ou incondicional):
– < S > : executa comandos “S” atomicamente
 Ação atômica com condição (ou condicional):
– < await (B); S >: espera ocorrência da
condição B, então executa “S” atomicamente
 Exemplo: < await(s>0); s=s-1 > < s= s+1 >
– Que operações são estas acima?
 Exemplo: < x= x+1; y= y+1; >
– “Estados internos” (como x já incrementado e y
não) não são visíveis a outros processos.
 Regiões críticas são usadas para tratar blocos de
código conflitantes
– No entanto, definir a granularidade de uma
seção crítica não é simples

® MTOV 18
Exemplo: Maior elemento de
um vetor de inteiros
 Versão seqüencial:
int m = 0;
for [i=0 to n-1]
if (a[i]>m)
m=a[i];
 Versões incorretas:

co [i=0 to n-1] co [i=0 to n-1]


if (a[i]>m) if (a[i]>m)
m=a[i]; < m=a[i]; >
// write_set(i)= m // serialização arbitrária
 Versão com baixo  Melhor versão:
paralelismo: int m = 0;
int m = 0; co for [i=0 to n-1]
co for [i=0 to n-1] if (a[i]>m)
< if a[i] > m < if a[i] > m
m=a[i]; > m=a[i]; >
(muito parecido com (só sincroniza, se for
versão seqüencial) atualizar “m”)

® MTOV 19
Sincronizando Produtores e
Consumidores
 Copiar um vetor de inteiros “a” de um processo
para outro, usando um buffer compartilhado
 Buffer é uma único inteiro “buf” (compartilhado)
int buf, p=0, c=0; // p: inteiros produzidos
process Producer { // c: inteiros consumidos
int a[n];
while (p<n) {
< await (p==c); > // produzido já foi consumido
buf=a[p]; // “produz”
p=p+1;
}
process Consumer {
int b[n];
while (c<n) {
< await (p>c); > // produzido não foi consumido
b [c]=buf; // “consome”
c=c+1;
}
}
 Sincronização via await implica em uma espera
ocupada (“busy wait”)

® MTOV 20
Políticas de Escalonamento

 Vários processos, com várias ações candidatas a


execução. Como escolher/eleger uma ?
– Por meio de uma política de escalonamento
 Escalonamento incondicionalmente justo (fair):
uma ação atômica incondicional candidata a
execução é eventualmente executada
 Escalonamento fortemente justo:
– incondicionalmente justo
– Em <await B; S>, se B às vezes é falsa e às
vezes é verdade, então a ação não é sempre
selecionada quando sua condição é falsa.
 Escalonamento fracamente justo: quando não
garante a segunda condição acima.
 Exemplo:
bool continue= true, try= false;
co while (continue) { try = true; try= false; }
co <await (try) continue= false>;
 Se fortemente justo, programa sempre termina
 Se fracamente justo, pode nunca terminar
 Na prática, fortemente justo não é atendido

® MTOV 21
Locks e Barreiras (cap. 3)

 Como implementar uma seção crítica(SC) ?


– Resposta: por meio de < .... >
 Mas como implementar < .... > ?
– Resposta: por meio de locks
 Problema da SC:
process CS[i = 1 to n] {
while (true) {
entry protocol;
critical section;
exit protocol;
noncritical section;
}
}
 Propriedades:
– Exclusão Mútua (safety)
– Ausência de deadlocks/livelocks (safety)
– Ausência de atrasos desnecessários (safety)
– Entrada em tempo finito (liveness)

® MTOV 22
Seção Crítica via Locks

 lock: variável booleana que indica se algum


processo encontra-se na SC:
– lock= true: algum processo está na SC
– lock= false: nenhum processo está na SC
 SC com locks:
< await (!lock) lock = true; >
critical section
lock = false;
 Como implementar o await acima?
– Via uma instrução de hardware
 Test and Set:
bool TS(bool lock) {
< bool initial = lock;
lock = true;
return initial; >
}
 SC por meio de Test and Set:
while (TS(lock)) skip; // CSEnter
critical section; // TS(lock) retornou false; lock=true
lock = false; // CSExit

® MTOV 23
Seção Crítica via Locks
 Exclusão Mútua:
– Dois processos tentam entrar mesmo tempo
– Um deles executa TS(lock) primeiro e seta lock para
true (entrando na SC)
– Outro, executa TS(lock), retorna true e espera
 Ausência de deadlocks:
– Se 2 processos bloqueados, lock=true sempre
– Mas para lock ser true, significa que um processo
entrou na SC; ele tem que sair e fazer lock= false
 Ausência de atrasos desnecesários:
– Se processo fora da SC, então ele não pode
bloquear a entrada do outro
 Entrada em tempo finito:
– Requer escalonamento fortemente justo
– Nesse caso, assegura-se que “processo
atrasado” irá, em algum momento, chamar
TS(lock) quando lock=false
 Por fim, <await (B); S> é implementado como:
CSEnter;
while(!B) { CSExit; delay; CSEnter; }
S;
CSExit;

® MTOV 24
Algoritmo Tie-Breaker

 Solução fair para SC, com dois processos:


– “in1” e “in2”: processo está ou não na SC
– “last”: último proc. iniciou protocolo entrada
 Algoritmo:
bool in1= false, in2= false; int last= 1;
process P1 {
while (true) {
in1= true; last= 1;
while (in2 and last ==1) skip;
critical section;
in1= false;
non-critical section;
}
}
process P2 {
while (true) {
in2= true; last= 2;
while (in1 and last ==2) skip;
critical section;
in2= false;
non-critical section;
}
}

® MTOV 25
Algoritmo Tie-Breaker

 Exclusão Mútua:
– Se P1 na SC, (in2==false ou last==2)
– Logo, P2 não pode estar também na SC
 Interferência entre processos:
– Suponha que in2= false e P1 tente entrar
– Antes de P1 entrar, P2 faz in2= true.
– P1 entra, mas P2 não consegue pois last==2
 Fair:
– Se P1 e P2 disputando SC (in1= in2= true)
– P2 entrou (isto é, P1 setou last por último)
– P2 saiu e disputa SC (in2= true; last= 2)
– P1 entra (já que last==2)

 Algoritmo Tie-breaker: solução para n processos é


mais complexa.

® MTOV 26
Algoritmo do Ticket
 Solução fair para SC, com n processos
 Algoritmo da “senha” do banco: cliente chega ao
banco, pega uma senha e aguarda ser chamado
 Variáveis:
– “number”: núm. da próx. “senha” a distribuir
– “next”: núm. do próx. processo a ser atendido
– “turn[n]”: armazena senha dos n processos
 Algoritmo:
int number= 1, next=1; int turn[n];
process CS[i= 1 to n] {
while (true) {
< turn[i]= number; number++; >
< await (turn[i] == next); >
critical section;
< next++; >
non-critical section;
}
}
 Depende de uma instrução FA (fetch and add):
– FA(x, incr)= < int t= x; x=x+incr; return t; >
 Alternativa: usar “test and set” p/ implementar FA

® MTOV 27
Barreiras

 Ponto de sincronização: todos os processos


devem atingir este ponto para que a execução de
quaisquer deles possa prosseguir
 Barreiras usando um contador compartilhado:
– Quando processo atinge a barreira, contador é
incrementado
– Quando contador igual a “n”, abre-se barreira
 Exemplo:
int count=0
......
code to implement task i;
< count++; > // barreira
< await (count==n); >
 Problema: como reutilizar a barreira, isto é, como
resetar o contador ?
– Contador deve ser resetado antes que um
processo tente incrementá-lo de novo
 Outro problema: contenção acesso à memória
– (n-1) processos inspecionando contador,
esperando último processo atingir a barreira

® MTOV 28
Barreiras com Processo
Coordenador
 Evita contenção no acesso à memória
– “arrive[i]”: indica se processo i atingiu barreira
– “continue[i]”:indica pode prosseguir execução
 Algoritmo:
int arrive[n], continue[n]; // inicializados com zero
process Worker[i=1 to n] {
while (true) {
code to implement task i;
arrive[i]=1; // anuncia chegada
< await (continue[i]==1); > // permissão prosseguir
continue[i]=0; // reseta flag
}
}
process Coordinator
while (true) {
for [i=1 to n] {
< await (arrive[i]==1); > // aguarda todos chegarem
arrive[i]=0;
}
for [i=1 to n] continue[i]=1; // avisa podem prosseguir
}
}
 Quem testa um flag, deve resetá-lo

® MTOV 29
Semáforos (cap. 4)

 Implementação de SC não é trivial


 Logo, necessita-se de mecanismos de
sincronização de mais alto nível, tais como
– < S > e < await (B); S > (linguagem SR)
– Semáforos (cap. 4) e monitores (cap. 5)
 Semáforos (Dijkstra, 1968): variável inteira
compartilhada entre processos.
 Operações sobre semáforos:
– sem s; (inicialização)
– P(s): < await (s>0) s = s-1; > (ou wait)
– V(s): < s = s+1; > (ou signal)
 Exemplo 1: Implementação de Seção Crítica
sem livre=1; // livre= 1: SC livre
process CS[i=1 to n] {
while (true){
wait(livre);
critical section; // livre= 0: processo na SC
signal(livre);
noncritical section;
}
}

® MTOV 30
Semáforos

 Exemplo 2: Erros na utilização de semáforos:


– wait(livre); CS; wait(livre); (deadlock)

– signal(livre); CS; wait(livre); (sem exclusão)


 Semáforo binário: valor é sempre 0 ou 1
 Semáforo sinalizador: inicializado com zero
– Esperando um evento: wait(s)
– Sinalizando evento: signal(s)
 Exemplo 3: Barreiras (semáforo sinalizador)
sem arrive1= 0, arrive2= 0;
process Worker1 {
“executa task1”;
signal(arrive1);
wait(arrive2);
............ // assegura-se que task1 e task2 executadas
}
process Worker2 {
“executa task2”;
signal(arrive2);
wait(arrive1);
........... // assegura-se que task1 e task2 executadas
}

® MTOV 31
Produtor/Consumidor

 M prod., N consumidores e buffer com um inteiro


 Semáforos:
– empty: usado para sinalizar que buffer vazio
– full: usado para sinalizar que buffer cheio
int buf;
sem empty=1, full=0; // inicialmente buffer vazio
process Producer[i=1 to M] {
while(true) {
“produce data“
P(empty); // espera buffer vazio para produzir

buf=data;
V(full); // após produzir, sinaliza que buffer está cheio
}
}
process Consumer[j=1 to N] {
while(true) {
P(full); // espera buffer cheio para consumir
result=buf;
V(empty); // após consumir, sinaliza buffer vazio
}
}
 Semáforos particionados (split): um processo executa
P(s);...;V(r) e outro processo P(r);...;V(s)

® MTOV 32
Produtor/Consumidor com
Buffer de Tamanho n
 Variáveis:
– rear: índice onde será inserido próximo valor
– front: índice de onde será lido próximo valor
 Valores armazenados no buffer: iniciam em front e
terminam em rear-1, de forma circular
 Possível intercalar “produtores” com
“consumidores” (em posições diferentes)
 Intercalações impossíveis: “produtores com
produtores”, “consumidores com consumidores”
 Semáforos para criar SC: mutexP e mutexC
– Produtor: P(mutexP); “produz”; V(mutexP)
– Consum.: P(mutexC); “consome”; V(mutexC)
 Semáforos para sinalizar mudanças no buffer:
– empty: armazena número de posições vazias
– full: armazena número de posições ocupadas
– empty+full == n (por construção do algoritmo)
 Antes produzir, deve existir slot vazio: P(empty)
 Após produzir, um slot ficou ocupado: V(full)
 Antes consumir, deve existir slot ocupado: P(full)
 Após consumir, um slot ficou vazio: V(empty)

® MTOV 33
Produtor/Consumidor com
Buffer de Tamanho n
int buf[n];
int front=0, rear=0;
sem empty=n, full=0;
sem mutexP=1, mutexC=1 # for mutual exclusion
process Producer[i=1 to M] { # M producers
while(true) {
“produce data“
P(empty); # block if buffer full - empty is 0
P(mutexP); # block multiple producers to access buffer
buf[rear]=data; rear=(rear+1)%n;
V(mutexP); # leave critical section
V(full); # increment full counter - split semaphore
}
}
process Consumer[j=1 to N] { # N consumers
while(true) {
P(full); # block if buffer empty - full is 0
P(mutexC); # block multiple consumers to access buffer
result=buf[front]; front=(front+1)%n;
V(mutexC); # leave critical section
V(empty); # increment empty counter - split semaphore
“consume result”
}
}

® MTOV 34
Jantar dos Filósofos

 Filósofos pensam, comem, pensam, comem, ...


 Para comer, precisam de dois garfos
 Existem apenas cinco garfos na mesa
 Solução: vetor de semáforos representa garfos
– sem fork[5]= {1,1,1,1,1} # garfos estão livres
 Pegar garfo i: P(fork[i])
– < await fork[i] > 0; fork[i]= fork[i] -1; >
 Liberar garfo i: V(fork[i]);
– < fork[i]= fork[i]+1; >
 Filósofos pegam o garfo da esquerda e, logo em
seguida, o garfo da direita
P(fork[i]);
P(fork[i+1] mod 5);
“eat”
V(fork[i]);
V(fork[i+1] mod 5);
® MTOV 35
Jantar dos Filósofos

 Deadlock: filósofos morrem de fome quando cada


um pega o garfo à sua esquerda e se recusa a
liberá-lo
 Deadlock: é um ciclo de espera
– P1 espera por recurso com P2;
– P2 espera por recurso com P3;
– .....
– Pn espera por recurso com P1
 Solução: quebrar o ciclo acima
 Filósofo 4 pega primeiro o garfo à sua direita
(isto é, garfo à esquerda do Filósofo 3)
 Assim, Filósofo 3 e 4 disputam este garfo: quem
ganhar, come (e evita-se o deadlock)
 Outra solução: limitar a quatro o número de
filósofos que podem comer simultaneamente
– sem room= 4;
– Ao ficar com fome: P(room)
– Após comer: V(room)

® MTOV 36
Leitores e Escritores

 Dois tipos de processos -- leitores e escritores --


compartilham um banco de dados:
– Escritores devem ter acesso exclusivo ao BD
– Vários leitores podem acessar juntos o BD
 Escritor: acessa BD em uma SC (semáforo rw=1)
– P(rw); “write database”; V(rw)
 Leitor:
– nr: número de leitores ativos (acessando BD)
– Ao inciar leitura: incrementar nr
– Ao terminar leitura: decrementar nr
– Se primeiro leitor a usar (nr == 1): P(rw)
– Se último leitor a usar (nr == 0): V(rw)
 Primeira versão do leitor:
process Reader[i= 1 to M] {
while(true) {
< nr++; if (nr == 1) then P(rw); >
"read database"
< nr--; if (nr == 0) then V(rw); >
}
}
 Semáforo mutexR: exclusão mútua no acesso à
variável nr

® MTOV 37
Leitores e Escritores

int nr= 0; # number of active readers


int rw= 1; # lock for reader/writer exclusion
sem mutexR= 1; # lock for reader access to nr
process Reader[i= 1 to M] {
while(true) {
P(mutexR);
nr++; if (nr == 1) then P(rw); # if first, get lock
V(mutexR)
"read database"
P(mutexR)
nr--; if (nr == 0) then V(rw); # if last, release lock
V(mutexR)
}
}
process Writer[j= 1 to n] {
while (true) {
....
P(rw); "write the database”; V(rw);
}
}
 Esta solução não é fair, pois privilegia leitores
 Se um leitor usando; e se um leitor e um escritor
desejarem usar, então leitor vai usar
 Um fluxo de leitores atrasa um escritor

® MTOV 38
Monitores (cap. 5)

 Mecanismo de sincronização de mais alto nível,


quando comparado com semáforos
– C.A.R Hoare (1974) e B. Hansen (1975)
 Adotado em várias linguagens, como em Java
 Monitor= variáveis + procedimentos (= objeto)
– Monitor: mecanismo de abstração de dados
 Suporta automaticamente exclusão mútua: em um
dado instante, apenas um processo pode executar
procedimentos de um monitor
 Logo, compilador (e não o programador) fica
responsável por assegurar exclusão mútua
 Sintaxe:
monitor <name> {
<declaração de variáveis>
<procedimentos>
}
 Chamando procedimento de um monitor:
call <name>.<opname> ( <argumentos> )
 Variáveis de um monitor são sempre privadas

® MTOV 39
Monitores

 Sincronização condicional (await) em monitores:


implementada usando “variáveis condicionais”
– Sintaxe: cond cv; // cv é uma var. condicional
 Valor de uma var. condicional: fila de processos
 Manipulando variáveis condicionais:
– empty(cv): fila de cv vazia ?
– wait(cv): processo se “auto-enfilera” na fila de
cv e libera o monitor
– signal(cv): “acorda” o processo na 1a posição
da fila associada a cv
 Existem duas semânticas para um signal:
– Signal and Continue (SC): quem emitiu o signal
continua executando (processo sinalizado
executa mais tarde)
– Signal and wait (SW): quem emitiu o signal
“libera” o monitor (processo sinalizado então
inicia execução)
 Exemplos a seguir assumem semântica SC. Usada
no livro do Andrews e em Java

® MTOV 40
Exemplo 1: Semáforos

 Implementando semáforos usando monitores:


monitor semáforo {
int s= 0; // valor do semáforo
cond pos; // fila de processos esperando
procedure P() {
while (s == 0) wait(pos);
s= s - 1;
}
procedure V() {
s= s + 1;
signal(pos);
}
}

® MTOV 41
Exemplo 2:
Produtor/Consumidor
monitor Buffer {
int buf[n]; # buffer
int primeiro=0, # primeira posição ocupada em buf
ultimo=0; # primeira posição livre em buf
cont=0; # número de slots ocupados em bh
cond full, # produtores esperando porque buf cheio
empty; # consumidores esperando porque buf vazio

procedure inserir (int dado) {


while (cont==n) wait(full); # enquanto buf cheio, esperar
buf[ultimo]=dado;
ultimo=(ultimo+1) % n;
cont++;
signal (empty); # acorda consumidor esperando
}

procedure retirar (int &dado) {


while (cont==0) wait (empty); # enquanto buf vazio, espera
dado=buf[primeiro];
primeiro=(primeiro+1) % n;
cont--;
signal(full); # acorda produtor esperando
}
}

® MTOV 42
Estudo de Caso: Threads em
Java
 Solução 1: subclasse de Thread
class PingPong extends Thread {
private String palavra;
public PingPong(String palavra) { this.palavra= palavra; }
public void run() { // threads devem implementar o método run
try {
for(int i= 0; i < 1000; i++) { System.out.print(palavra + " "); Thread.sleep(100); }
}
catch (InterruptedException e) { return; }
}
public static void main(String [] args) {
Thread t1= new PingPong("ping");
Thread t2= new PingPong("PONG");
t1.start(); t2.start();
}
}
 Solução 2: implementar interface Runnable
class PingPong implements Runnable {
..... // mesmo código do exemplo anterior
public static void main(String [] args) {
Runnable ping= new PingPong("ping");
Runnable pong= new PingPong("PONG");
new Thread(ping).start();
new Thread(pong).start();
}
}

® MTOV 43
Sincronização
 Seja o exemplo abaixo:
class ContaBancaria {
private double saldo;
public double getSaldo() { return saldo; }
private setSaldo(double saldo) { this.saldo= saldo; }
public doube depositar(double valor)
{ double s= getSaldo(); s= s+valor; setSaldo(s); }
}
 Se duas threads chamarem depositar(100):
Thread 1 Thread 2 Saldo
s= getSaldo(); (s= 100) 100
s= getSaldo() ; (s= 100)
s= s+ valor; (s= 200)
s= s+ valor; (s= 200)
setSaldo(s); 200
setSaldo(s); 200

 Como criar seções críticas? Por meio da palavra


synchronized
class ContaBancaria {
private double saldo; ........
public synchronized doube depositar(double valor)
{ saldo= saldo + valor; }
public synchronized doube retirar(double valor)
{ saldo= saldo - valor; }
}
 Classe com métodos sincronizados: monitor
® MTOV 44
Sincronização

 Todo objeto em Java possui um lock: informa se o objeto foi


ou não “travado” por alguma thread
 Seja a chamada p.m(): (m é synchronized)
– Antes executar m, thread deve adquirir lock de p (i.e.,
travar p). Se p já estiver travado, thread espera até ser
destravado.
– Quando thread terminar execução de m, ela libera o lock
de p
– Durante execução de m, thread pode chamar outros
métodos synchronized desse objeto (sem esperar)
 Como colocar uma thread para dormir: por meio de wait()
– Chamado por thread de posse do lock de um objeto p
– Libera-se o lock deste objeto
– Execução desta thread é suspensa até uma execução
de notify() ou notifyAll() sobre p
– Ao ser “acordada”, thread volta a disputar o lock de p.
Execução reinicia na linha seguinte ao wait()

® MTOV 45
Sincronização
 Como “acordar” uma thread? notify() ou notifyAll()
 notify(): chamado por thread de posse do lock de um
objeto p
– Acorda (arbitrariamente) uma das threads que
estejam “dormindo” sobre este objeto
– Thread acordada volta a disputar lock de p
 notifyAll(): acorda todas as threads que estejam
dormindo sobre um determinado objeto.
 Todo objeto em Java possui dois conjuntos:
– Conjunto de entrada: threads que estão
bloqueadas, disputando o lock desse objeto (i.e.,
chamaram um método sincronizado, mas não
iniciaram a execução do mesmo)
– Conjunto de espera: threads cuja execução está
suspensa
 Operações:
– wait(): move a thread corrente para o conjunto de
espera do objeto sobre o qual a operação foi
chamada.
– notify(): move uma thread arbitrária do conjunto de
espera para o conjunto de entrada
– notifyAll: move todas as threads do conjunto de
espera para o conjunto de entrada
 Quando uma thread libera o lock de um objeto, uma
thread arbitrária do conjunto de entrada é desbloqueada

® MTOV 46
Produtor/Consumidor em Java

public class Produtor extends Thread {


private Buffer buf; private int numero;
public Produtor(Buffer buf, int numero) {
this.buf= buf; this.numero = numero;
}
public void run() {
for (int i = 0; i < 10; i++) { // armazena 10 valores no buffer
buf.put(i);
System.out.println("Produtor #" + numero + " put: “+ i);
try { sleep((int)(Math.random() * 100)); }
catch (InterruptedException e) { }
}
}
}
public class Consumidor extends Thread {
private Buffer buf; private int numero;
public Consumidor(Buffer buf, int numero) {
this.buf= buf; this.numero = numero;
}
public void run() { // retira 10 valores do buffer
int x;
for (int i = 0; i < 10; i++) {
x = buf.get();
System.out.println("Consumidor #"+ numero + " get: " + x);
}
}

® MTOV 47
Produtor/Consumidor em Java

 Suponha um buffer com capacidade para apenas um


inteiro e métodos get() e put():

public class Buffer {


private int valor; private boolean cheio = false;
public synchronized int get() {
while (!cheio) {
try { wait(); } catch (InterruptedException e) { }
}
cheio= false; notifyAll(); return valor;
}
public synchronized void put(int valor) {
while (cheio) {
try { wait(); } catch (InterruptedException e) { }
}
this.valor= valor; cheio= true; notifyAll();
}
}
 No método get(), ao sair do wait, thread deve voltar a testar se
buffer está vazio. Pode ser que outro consumidor tenha obtido o
lock do objeto na frente e esvaziado o buffer. Se isso tiver
acontecido, thread volta a dormir
 No método put(), ao sair do wait, thread deve voltar a testar se
buffer está cheio. Pode ser que outro produtor tenha obtido o lock
do objeto na frente e ocupado o buffer. Se isso tiver acontecido,
thread volta a dormir

® MTOV 48
Leitores e Escritores

 Assuma que threads leitoras e escritoras


compartilham um objeto de uma classe BD, a qual
possui um atributo inteiro x e dois métodos: read
(retorna o valor de x) e write (incrementa o valor de
x). A solução proposta deve permitir leituras
concorrentes.
class BD {
private int x; private int nr= 0;
private synchronized void startRead() { nr++; }
public synchronized void endRead() { nr--; if (nr==0)
notify(); }
public int read() {
int t; startRead(); t= x; endRead(); return t;
}
public synchronized void write() {
while (nr > 0) wait();
x++; notify();
}
}
 Se um escritor ativo: leitor é bloqueado no startRead()
 Se existe leitor ativo: novos leitores conseguem ler; novo
escritor fica bloqueado no while
 notify de endRead: desbloquear escritores
 notify de write; desbloquear outros escritores

® MTOV 49
Comando synchronized

 Permite adquirir o lock de qualquer objeto (por uma


duração inferior à execução completa de um
método)
 Exemplo:
void abs (int [] v) {
synchronized (v) { // garante que v é acessado
em uma seção crítica
for (int i= 0; i < v.lenghth; i++)
if (v[i] < 0) v[i]= -v[i];
}
}

® MTOV 50
Jantar dos Filósofos em Java

class Filosofo extends Thread {


Garfo esq; Garfo dir; ....
public void run() { Filósofo pensa; pega garfo da direita e
while (true) { da esquerda; come e devolve ambos os
sleep(1000); garfos
dir.get(); esq.get();
sleep(1000) ;
dir.put(); esq.put();
}
}
Class Garfo {
private boolean livre= true;
public synchronized void get() {
while (!livre) wait(); // faltou tratar exceção
livre= false;
}
public synchronized void put() {
livre= true; notify();
}
}
Garfo inicialmente está livre; get() fica
suspenso enquanto garfo ocupado; put()
libera garfo e acorda possível filósofo
que esteja bloqueado esperando pelo
mesmo
® MTOV 51
Deadlocks: Exemplo 2

public class BankAccount {


private float balance;
public synchronized void deposit(float amount)
{ balance += amount; }
public synchronized void withdraw(float amount)
{ balance -= amount; }
public synchronized void transfer(float amount,
BankAccount target) {
withdraw(amount); target.deposit(amount);
}
} t1 t2

aliceAccount bobAccount

transfer()
transfer()

® MTOV
withdraw()

deposit() ? withdraw()

deposit()
52
Deadlocks: Exemplo 2

 (Pseudo) Solução 1: ainda sujeita a deadlock


public void transfer(Account from, Account to, int amount) {
synchronized (from) {
synchronized (to) {
from.withdraw(amount);
to.deposit(amount);
}
} Thread 1: transfer(a1,a2) Thread 2: transfer(a2,a1)
} lock a1
lock a2
blocked lock a2
blocked lock a1
 Solução 2: livre de deadlock
public void transfer(Account from, Account to, int amount) {
Account first; Account second;
if (from.id < to.id) {
first = a1; second = a2;
}
else { first = a2; second = a1; }
synchronized (first) {
synchronized (second) {
from.withdraw(amount);
to.deposit(amount);
}
}
}

® MTOV 53

Você também pode gostar