Professor: Gabriel Soares Baptista
Esta aula revisa os tres blocos principais da segunda avaliacao:
A ideia e sair daqui sabendo reconhecer os problemas, calcular exemplos pequenos e justificar respostas no estilo da prova.
O conteudo exige tres tipos de habilidade:
| Tipo | Exemplo |
|---|---|
| Conceitual | explicar por que contador++ nao e atomico |
| Interpretacao de codigo | identificar uma condicao de corrida ou deadlock |
| Calculo | montar linha do tempo de escalonamento ou sequencia segura |
Nao basta decorar nomes de algoritmos. A prova tende a cobrar o comportamento do sistema em uma situacao concreta.
Processos possuem espacos de enderecamento separados. Para cooperarem, precisam de mecanismos de IPC.
Exemplos comuns:
O IPC resolve a troca de dados, mas tambem cria o problema da sincronizacao.
Um pipe conecta uma ponta de escrita a uma ponta de leitura.
int fd[2];
pipe(fd);
if (fork() == 0) {
close(fd[1]);
read(fd[0], buffer, sizeof(buffer));
} else {
close(fd[0]);
write(fd[1], "mensagem", 9);
}
Perguntas importantes:
Memoria compartilhada costuma ser mais rapida que pipes porque evita copiar dados pelo kernel a cada mensagem.
Mas ela traz um risco:
Se dois processos ou threads modificam a mesma estrutura ao mesmo tempo, o resultado pode depender da ordem de escalonamento.
Isso e uma condicao de corrida.
Uma condicao de corrida ocorre quando:
Exemplo classico:
contador++;
Parece uma operacao unica, mas normalmente envolve carregar, incrementar e salvar.
#include <pthread.h>
#include <stdio.h>
long contador = 0;
void* incremento(void* arg) {
for (int i = 0; i < 500000; i++) {
contador++;
}
return NULL;
}
Se duas threads executam isso, o valor esperado seria 1000000.
Mas o valor final pode ser menor, porque atualizacoes se perdem.
Imagine contador = 10.
| Passo | Thread A | Thread B |
|---|---|---|
| 1 | le 10 |
|
| 2 | le 10 |
|
| 3 | calcula 11 |
calcula 11 |
| 4 | salva 11 |
|
| 5 | salva 11 |
Duas incrementacoes ocorreram, mas o contador aumentou apenas uma unidade.
Regiao critica e o trecho de codigo que acessa recurso compartilhado e precisa ser protegido.
// regiao critica
contador++;
O objetivo da exclusao mutua e garantir que apenas uma thread ou processo execute a regiao critica por vez.
Mutex e uma trava para exclusao mutua.
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* incremento(void* arg) {
for (int i = 0; i < 500000; i++) {
pthread_mutex_lock(&lock);
contador++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
Com o mutex, a operacao composta fica protegida.
Semaforos generalizam a ideia de controle de acesso.
Operacoes classicas:
down, P ou wait: tenta consumir uma permissaoup, V ou signal: devolve uma permissaoUsos comuns:
| Caso | Mecanismo |
|---|---|
| proteger estrutura compartilhada | mutex |
| contar vagas em buffer | semaforo |
| contar itens produzidos | semaforo |
O produtor-consumidor combina mutex e semaforos.
sem_t empty; // vagas livres
sem_t full; // itens disponiveis
pthread_mutex_t mutex;
// produtor
sem_wait(&empty);
pthread_mutex_lock(&mutex);
insere_item();
pthread_mutex_unlock(&mutex);
sem_post(&full);
O semaforo controla o estado do buffer. O mutex protege a estrutura interna do buffer.
No codigo abaixo, marque a regiao critica e diga qual problema pode ocorrer.
int saldo = 100;
void saque(int valor) {
if (saldo >= valor) {
saldo = saldo - valor;
}
}
O que acontece se duas threads chamarem saque(80) ao mesmo tempo?
A regiao critica e o acesso conjunto a saldo:
if (saldo >= valor) {
saldo = saldo - valor;
}
Problema possivel:
saldo = 10080Escalonar e decidir quem usa a CPU, quando e por quanto tempo.
O escalonador tenta equilibrar:
| Metrica | Significado |
|---|---|
| Tempo de espera | tempo parado na fila de prontos |
| Tempo de retorno | termino menos chegada |
| Tempo de resposta | primeira execucao menos chegada |
| Vazao | processos finalizados por unidade de tempo |
| Utilizacao da CPU | tempo fazendo trabalho util |
$$T_{retorno} = T_{termino} - T_{chegada}$$
Executa na ordem de chegada.
Vantagens:
Problema:
Todos chegam em t=0 na ordem P1, P2, P3.
| Processo | Burst |
|---|---|
| $P_1$ | 24 ms |
| $P_2$ | 3 ms |
| $P_3$ | 3 ms |
Tempos de espera:
$$T_{espera}^{medio}=\frac{0+24+27}{3}=17\text{ ms}$$
Escolhe primeiro o processo com menor burst de CPU.
Quando todos chegam juntos, minimiza o tempo medio de espera.
Limites:
| Processo | Burst |
|---|---|
| $P_1$ | 24 ms |
| $P_2$ | 3 ms |
| $P_3$ | 3 ms |
Ordem SJF: $P_2, P_3, P_1$
Tempos de espera:
$$T_{espera}^{medio}=\frac{0+3+6}{3}=3\text{ ms}$$
E a versao preemptiva do SJF.
Se chega um processo novo com tempo restante menor que o processo atual, o atual e interrompido.
Bom para:
Custo:
Cada processo recebe um quantum.
Se nao terminar dentro do quantum, volta para o fim da fila.
E muito usado como base de sistemas interativos.
| Quantum curto | Quantum longo |
|---|---|
| melhor resposta | menos overhead |
| mais trocas de contexto | mais parecido com FCFS |
| pode desperdicar CPU | pode piorar interatividade |
Todos chegam em t=0, quantum 4 ms.
| Processo | Burst |
|---|---|
| $P_1$ | 10 ms |
| $P_2$ | 4 ms |
| $P_3$ | 5 ms |
Linha do tempo:
0 4 8 12 16 18 19
| P1 | P2 | P3 | P1 | P3 | P1 |
Terminos:
No escalonamento por prioridades, o processo mais prioritario roda primeiro.
Risco:
Solucao comum:
Considere os processos abaixo, todos chegando em t=0.
| Processo | Burst |
|---|---|
| $P_1$ | 6 ms |
| $P_2$ | 8 ms |
| $P_3$ | 7 ms |
| $P_4$ | 3 ms |
Calcule o tempo medio de espera usando SJF.
Ordem SJF:
$$P_4 \rightarrow P_1 \rightarrow P_3 \rightarrow P_2$$
Tempos de espera:
$$T_{espera}^{medio}=\frac{0+3+9+16}{4}=7\text{ ms}$$
Deadlock ocorre quando um conjunto de processos fica bloqueado esperando eventos que apenas processos do mesmo conjunto podem causar.
Nao e apenas lentidao.
E ausencia estrutural de progresso.
Para haver deadlock, quatro condicoes precisam ocorrer ao mesmo tempo:
Se quebrarmos qualquer uma delas, impedimos o deadlock.
pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;
void* threadA(void* arg) {
pthread_mutex_lock(&lock1);
sleep(1);
pthread_mutex_lock(&lock2);
return NULL;
}
void* threadB(void* arg) {
pthread_mutex_lock(&lock2);
sleep(1);
pthread_mutex_lock(&lock1);
return NULL;
}
O sleep(1) nao causa o deadlock sozinho. Ele apenas aumenta a chance de expor o problema.
No exemplo anterior:
| Condicao | Onde aparece |
|---|---|
| Exclusao mutua | mutex so pode ter um dono |
| Posse e espera | A segura lock1 e espera lock2 |
| Nao preempcao | o sistema nao toma mutex a forca |
| Espera circular | A espera B, B espera A |
Na deteccao, o sistema permite que o deadlock aconteca e depois procura ciclos ou estados sem progresso.
Estrategias comuns:
Depois de detectar, o sistema precisa recuperar.
Quando existem varios tipos de recursos, o grafo pode ficar grande. Uma forma pratica de detectar deadlock e usar matrizes.
Estruturas principais:
Available: recursos livres agoraAllocation: recursos que cada processo ja possuiRequest: recursos que cada processo ainda esta esperandoA pergunta da deteccao e:
Com os recursos livres agora, existe algum processo que consiga terminar e devolver o que possui?
Passos:
Work = AvailableRequest <= WorkAllocation desse processo em WorkSe algum processo ficar sem conseguir terminar, ele esta envolvido em deadlock ou preso por falta de recursos que ninguem conseguira liberar.
Considere quatro tipos de recurso: R1, R2, R3, R4.
$$Available = (2,1,0,0)$$
| Processo | Allocation | Request |
|---|---|---|
| $P_1$ | $(0,0,1,0)$ | $(2,0,0,1)$ |
| $P_2$ | $(2,0,0,1)$ | $(1,0,1,0)$ |
| $P_3$ | $(0,1,2,0)$ | $(2,1,0,0)$ |
Request significa: o que o processo ainda esta esperando para conseguir prosseguir.
Comecamos com:
$$Work = Available = (2,1,0,0)$$
Testamos cada processo:
| Processo | Request | Cabe em Work? |
|---|---|---|
| $P_1$ | $(2,0,0,1)$ | nao, falta R4 |
| $P_2$ | $(1,0,1,0)$ | nao, falta R3 |
| $P_3$ | $(2,1,0,0)$ | sim |
Entao simulamos $P_3$ terminando.
$P_3$ possui:
$$Allocation(P_3) = (0,1,2,0)$$
Ao terminar, ele devolve esses recursos:
$$Work = (2,1,0,0) + (0,1,2,0) = (2,2,2,0)$$
Agora marcamos $P_3$ como finalizado e repetimos o teste.
Agora:
$$Work = (2,2,2,0)$$
Testamos os restantes:
| Processo | Request | Cabe em Work? |
|---|---|---|
| $P_1$ | $(2,0,0,1)$ | nao, falta R4 |
| $P_2$ | $(1,0,1,0)$ | sim |
Entao $P_2$ pode terminar.
$P_2$ possui:
$$Allocation(P_2) = (2,0,0,1)$$
Ao terminar:
$$Work = (2,2,2,0) + (2,0,0,1) = (4,2,2,1)$$
Agora $P_1$ tambem consegue terminar, pois:
$$Request(P_1) = (2,0,0,1) \leq (4,2,2,1)$$
A ordem encontrada foi:
$$P_3 \rightarrow P_2 \rightarrow P_1$$
Como todos conseguem terminar nessa simulacao, o sistema nao esta em deadlock.
Ideia importante:
Exemplo com um unico tipo de recurso:
| Processo | Allocation | Request |
|---|---|---|
| $P_1$ | 1 | 1 |
| $P_2$ | 1 | 1 |
$$Available = 0$$
Nenhum processo consegue receber o que pediu. Como ninguem termina, ninguem devolve recurso.
Nesse caso, a deteccao conclui que existe deadlock entre $P_1$ e $P_2$.
Opcoes apos detectar um deadlock:
Na pratica, a escolha depende de custo, prioridade, risco e impacto para o usuario.
Prevencao muda as regras para tornar deadlock impossivel.
| Condicao atacada | Estrategia |
|---|---|
| Exclusao mutua | transformar recurso em compartilhavel, quando possivel |
| Posse e espera | pedir todos os recursos de uma vez |
| Nao preempcao | liberar o que tem quando nao conseguir mais |
| Espera circular | impor ordem global de aquisicao |
Se todas as threads pegam locks na mesma ordem, a espera circular nao fecha.
void* threadA(void* arg) {
pthread_mutex_lock(&lock1);
pthread_mutex_lock(&lock2);
pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock1);
return NULL;
}
void* threadB(void* arg) {
pthread_mutex_lock(&lock1);
pthread_mutex_lock(&lock2);
pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock1);
return NULL;
}
O banqueiro evita entrar em estados inseguros.
Ele concede um pedido somente se, depois da concessao, ainda existir uma sequencia em que todos possam terminar.
Estruturas usadas:
Available: recursos disponiveisMax: demanda maxima declaradaAllocation: recursos ja alocadosNeed = Max - AllocationOs dois algoritmos parecem parecidos porque ambos simulam processos terminando e devolvendo recursos.
A diferenca esta na pergunta feita:
| Algoritmo | Pergunta central | Quando roda? |
|---|---|---|
| Deteccao matricial | Ja existe deadlock agora? | depois que recursos ja foram concedidos |
| Banqueiro | Se eu conceder este pedido, o estado continuara seguro? | antes de conceder um novo pedido |
Deteccao e diagnostico. Banqueiro e controle preventivo por simulacao.
Na deteccao matricial, o sistema usa:
Available: o que esta livre agoraAllocation: o que cada processo ja possuiRequest: o que cada processo esta pedindo agoraNo banqueiro, o sistema tambem precisa conhecer a demanda maxima:
Max: maximo que cada processo pode pedirNeed = Max - Allocation: quanto ainda pode vir a pedirPor isso o banqueiro e mais exigente: ele precisa que os processos declarem seu consumo maximo antecipadamente.
Deteccao matricial:
Banqueiro:
Importante: estado inseguro no banqueiro nao significa deadlock imediato. Significa que, se o sistema continuar concedendo recursos daquele jeito, pode chegar a um deadlock.
Passos:
Work = AvailableNeed <= WorkWorkSe todos terminam, o estado e seguro.
Se nao ha sequencia completa, o estado e inseguro.
Um unico tipo de recurso.
| Processo | Maximo | Alocado | Necessidade |
|---|---|---|---|
| $P_1$ | 9 | 3 | 6 |
| $P_2$ | 4 | 2 | 2 |
| $P_3$ | 7 | 2 | 5 |
$$Available = 3$$
Sequencia segura:
$$P_2 \rightarrow P_3 \rightarrow P_1$$
Na proxima semana teremos o feriado e, em seguida, a Avaliacao C2. Vou disponibilizar no AVA uma atividade avaliativa semelhante ao esquema da prova, com questoes conceituais, interpretacao de codigo e exercicios de escalonamento e deadlocks para orientar a preparacao final.