Computação Paralela e
Sistemas Distribuídos
Prof.: Felipe Domingos
Universidade de Itaúna Faculdade de Engenharia
Programa do Curso
Parte I: Programação Mul:thread/Paralela
– Arquiteturas, Conceitos e Caracterís:cas, [1] – Implementação de Seções Crí:cas [1]
– Semáforos e Monitores [1] – Estudo de Caso: Java [4]
Parte II: Programação Distribuída
– Conceitos e Caracterís:cas [1,2,3] – Middlewares [3]
– Estudo de Caso: Java RMI [3] e CORBA [3] – Algoritmos Distribuídos [2]
Obje:vo: conciliar teoria (fundamentos) tecnologia
(ferramentas) e prá:ca (problemas)
Bibliografia:
[1] Andrews, G. Founda'ons of Mul'threaded, Parallel, and
Distributed Programming, Addison-‐Wesley, 2000.
[2] Couloris, G., Dollimore, J., Kindberg, T. Distributed Systems
Concepts and Design (Third Edi:on). Addison-‐Wesley, 2001.
[3] Emmerich, W. Engineering Distributed Objects. John Wiley & Sons, 2000.
[4] Arnold, K. et al. The Java Programming Language, Third Edi:on, Addison-‐Wesley, 2000.
Programação Concorrente (cap.
1)
Programas seqüenciais: único fluxo execução
Programa concorrentes:vários fluxos execução
– Mul:thread, Paralelos ou Distribuídos
Mul@thread:
– núm. fluxos execução > núm. processadores – Obje:vo: fazer várias coisas, mesmo tempo – Ex.:GUI, SO, SGBD, servidores Web etc
Paralelos:
– núm. fluxos execução = núm. processadores – Obje:vo: fazer uma tarefa mais rápido
– Ex.: mul:plicar matrizes, previsão tempo, processar/ gerar imagem, simulações etc
Distribuídos:
– fluxos de execução em nodos de uma rede – Obje:vo: compar:lhar recursos
– Ex.:aplicações C/S (mail, web, dados etc)
Comunicação/Sincronização:
– Mem. compar:lhada: mul:threads, paralelos – Troca de mensagens: todos os 3 :pos
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í:cas 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
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 compar:lhado
Arquiteturas Monoprocessadas:
– Programação seqüencial com interrupções – Núcleo de mul:programação (SO, VM, ou
biblioteca) cria um modelo de programação assíncrono
– Vários processos compar:lham CPU
Arquiteturas Mul@processadas:
– Memória Compar:lhada – Memória Distribuída
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 compar:lhada ou troca de
mensagens
Vantagem: barramento de alta velocidade, memória
compar:lhada facilita programação
Desvantagem: limitação no número de nodos
Mul@processadores com
Memória Compar@lhada
Mul@processadores 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
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 Pen:um III) – Sistema operacional: Linux
– PCs possuem discos IDE de 80 GB
– Obje:vo: performance + hardware barato
– Ver: Web Search for a Planet: The Google Cluster
Mul@plicação Matrizes
(Memória Compar@lhada)
Mul:processadores com memória compar:lhada
Idéia: calcular produtos internos paralelamente
Variáveis compar:lhadas:
double a[n,n], b[n,n], c[n,n]
Código da mul:plicaçã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
Mul@plicaçã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 ini:al 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]; ini:alize 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;
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 compar:lhar 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
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)
prin€ ( Erro na criação do pipe ); else ((pid = fork()) < 0)
prin€ ( 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 }
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
Sincronização
Sincronização: restringe as histórias possíveis
Exclusão mútua: requer a criação de ações atômicas via
so†ware
– Uma ação atômica de maior granularidade é chamada de seção crí:ca
Exemplo: < ... > cria uma seção crí:ca
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
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í:ca (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)
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 condi@on: 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 pa‰ern in line; if (pa‰ern is in line) write line;
read next line of input; }
Exemplo: grep paralelo
Versão incorreta (com conflitos):String line1;
read a line of input from stdin into line1; while (!EOF) {
co look for pa‰ern in line1; // read set= line1
if (pa‰ern 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 pa‰ern in line1; // read set= line1
if (pa‰ern 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
Sincronização
Execução de programa concorrente: intercalação de
ações atômicas
Sincronização: evita intercalações indesejadas
– Via seções crí:cas
– 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í:cas são usadas para tratar blocos de código
conflitantes
– No entanto, definir a granularidade de uma seção crí:ca não é simples
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]
if (a[i]>m)
m=a[i];
// write_set(i)= m
co [i=0 to n-‐1]
if (a[i]>m)
< m=a[i]; >
// serialização arbitrária
Melhor versão:
int m = 0;
co for [i=0 to n-‐1] if (a[i]>m)
< if a[i] > m m=a[i]; >
(só sincroniza, se for atualizar m )
Versão com baixo
paralelismo: int m = 0;
co for [i=0 to n-‐1] < if a[i] > m m=a[i]; >
(muito parecido com versão seqüencial)
Sincronizando Produtores e
Consumidores
Copiar um vetor de inteiros a de um processo para
outro, usando um buffer compar:lhado
Buffer é uma único inteiro buf (compar:lhado)
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; }
}
Polí@cas de Escalonamento
Vários processos, com várias ações candidatas a
execução. Como escolher/eleger uma ?
– Por meio de uma polí:ca 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 con:nue= true, try= false;
co while (con:nue) { try = true; try= false; } co <await (try) con:nue= false>;
Se fortemente justo, programa sempre termina
Se fracamente justo, pode nunca terminar
Locks e Barreiras (cap. 3)
Como implementar uma seção crí:ca(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; cri:cal sec:on; exit protocol; noncri:cal sec:on; } } Propriedades:
– Exclusão Mútua (safety)
– Ausência de deadlocks/livelocks (safety) – Ausência de atrasos desnecessários (safety) – Entrada em tempo finito (liveness)
Seção Crí@ca 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; >
cri:cal sec:on lock = false;
Como implementar o await acima?
– Via uma instrução de hardware
Test and Set:
bool TS(bool lock) { < bool ini:al = lock;
lock = true; return ini:al; > }
SC por meio de Test and Set:
while (TS(lock)) skip; // CSEnter
cri:cal sec:on; // TS(lock) retornou false; lock=true
Seção Crí@ca 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;
Algoritmo Tie-‐Breaker
Solução fair para SC, com dois processos:
– in1 e in2 : processo está ou não na SC – last : úl:mo 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; cri:cal sec:on; in1= false; non-‐cri:cal sec:on; } } process P2 { while (true) {
in2= true; last= 2;
while (in1 and last ==2) skip; cri:cal sec:on;
in2= false;
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 úl:mo) – P2 saiu e disputa SC (in2= true; last= 2) – P1 entra (já que last==2)
Algoritmo Tie-‐breaker: solução para n processos é mais
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); >
cri:cal sec:on; < next++; >
non-‐cri:cal sec:on; }
}
Depende de uma instrução FA (fetch and add):
Barreiras
Ponto de sincronização: todos os processos devem
a:ngir este ponto para que a execução de quaisquer deles possa prosseguir
Barreiras usando um contador compar:lhado:
– Quando processo a:nge 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 reu:lizar 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 úl:mo processo a:ngir a barreira
Barreiras com Processo
Coordenador
Evita contenção no acesso à memória
– arrive[i] : indica se processo i a:ngiu barreira – con:nue[i] :indica pode prosseguir execução
Algoritmo:
int arrive[n], con:nue[n]; // inicializados com zero
process Worker[i=1 to n] {
while (true) {
code to implement task i;
arrive[i]=1; // anuncia chegada
< await (con:nue[i]==1); > // permissão prosseguir
con:nue[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] con:nue[i]=1; // avisa podem prosseguir }
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
compar:lhada 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í:ca
sem livre=1; // livre= 1: SC livre
process CS[i=1 to n] { while (true){
wait(livre);
cri:cal sec:on; // livre= 0: processo na SC signal(livre);
noncri:cal sec:on; }
Semáforos
Exemplo 2: Erros na u:lizaçã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
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 }
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 exis:r slot vazio: P(empty)
Após produzir, um slot ficou ocupado: V(full)
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 mul'ple producers to access buffer buf[rear]=data; rear=(rear+1)%n;
V(mutexP); # leave cri'cal sec'on 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 mul'ple consumers to access buffer result=buf[front]; front=(front+1)%n;
V(mutexC); # leave cri'cal sec'on V(empty); # increment empty counter -‐ split semaphore
consume result
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
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)
Leitores e Escritores
Dois :pos de processos -‐-‐ leitores e escritores -‐-‐
compar:lham 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 a:vos (acessando BD) – Ao inciar leitura: incrementar nr
– Ao terminar leitura: decrementar nr – Se primeiro leitor a usar (nr == 1): P(rw) – Se úl:mo 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); > } }
Leitores e Escritores
int nr= 0; # number of ac've 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
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 automa:camente 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> )
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ân:cas para um signal:
– Signal and Con:nue (SC): quem emi:u o signal con:nua executando (processo sinalizado executa mais tarde)
– Signal and wait (SW): quem emi:u o signal libera o monitor (processo sinalizado então inicia
execução)
Exemplos a seguir assumem semân:ca SC. Usada no
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); } }
Exemplo 2: Produtor/
Consumidor
monitor Buffer {
int buf[n]; # buffer
int primeiro=0, # primeira posição ocupada em buf ul:mo=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[ul:mo]=dado;
ul:mo=(ul:mo+1) % n; cont++;
signal (empty); # acorda consumidor esperando }
procedure re:rar (int &dado) {
while (cont==0) wait (empty); # enquanto buf vazio, espera dado=buf[primeiro];
primeiro=(primeiro+1) % n; cont-‐-‐;
signal(full); # acorda produtor esperando }
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 (InterruptedExcep:on e) { return; } }
public sta:c 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 sta:c void main(String [] args) {
Runnable ping= new PingPong("ping"); Runnable pong= new PingPong("PONG"); new Thread(ping).start();
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; }
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á es:ver 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 no:fy() ou no:fyAll() sobre p
– Ao ser acordada , thread volta a disputar o lock de p. Execução reinicia na linha seguinte ao wait()
Sincronização
Como acordar uma thread? no@fy() ou no@fyAll()
no@fy(): 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
no@fyAll(): 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. – no:fy(): move uma thread arbitrária do conjunto de
espera para o conjunto de entrada
– no:fyAll: move todas as threads do conjunto de espera para o conjunto de entrada
Quando uma thread libera o lock de um objeto, uma
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 (InterruptedExcep:on 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() { // re:ra 10 valores do buffer int x;
for (int i = 0; i < 10; i++) { x = buf.get();
System.out.println("Consumidor #"+ numero + " get: " + x); }
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 (InterruptedExcep:on e) { } }
cheio= false; no:fyAll(); return valor; }
public synchronized void put(int valor) { while (cheio) {
try { wait(); } catch (InterruptedExcep:on e) { } }
this.valor= valor; cheio= true; no:fyAll(); }
}
No método get(), ao sair do wait, thread deve voltar a testar se buffer está
vazio. Pode ser que outro consumidor tenha ob:do o lock do objeto na frente e esvaziado o buffer. Se isso :ver 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 ob:do o lock do objeto na frente e ocupado o buffer. Se isso :ver acontecido, thread volta a dormir
Leitores e Escritores
Assuma que threads leitoras e escritoras compar:lham 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 permi:r leituras concorrentes.
class BD {
private int x; private int nr= 0;
private synchronized void startRead() { nr++; }
public synchronized void endRead() { nr-‐-‐; if (nr==0) no:fy(); } public int read() {
int t; startRead(); t= x; endRead(); return t; }
public synchronized void write() { while (nr > 0) wait();
x++; no:fy(); }
}
Se um escritor a:vo: leitor é bloqueado no startRead()
Se existe leitor a:vo: novos leitores conseguem ler; novo escritor fica
bloqueado no while
no:fy de endRead: desbloquear escritores no:fy de write; desbloquear outros escritores
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í:ca
for (int i= 0; i < v.lenghth; i++) if (v[i] < 0) v[i]= -‐v[i];
} }