#sincronizacao #deadlock #semaforo #estudos

Sincronização entre Processos

Arquitetura de Computadores e Sistemas Operacionais Tema 4

oi!! (≧◡≦) quando vários processos ou threads trabalham juntos, as coisas podem dar MUITO errado. um sobrescreve o dado do outro, dois tentam usar o mesmo recurso ao mesmo tempo, e de repente seu banco de dados tem saldo negativo. nesse tema a gente aprende a evitar esse caos. bora!! ☆~(ゝ。∂)

1. Condição de Corrida: A Briga pela Mesma Coisa

imagine duas pessoas tentando sacar dinheiro da mesma conta ao mesmo tempo. ambas leem o saldo (R$ 1000), ambas sacam R$ 100, ambas gravam o novo saldo (R$ 900). mas deveria ser R$ 800!! isso é uma condição de corrida (race condition): o resultado depende da ordem de execução, que é imprevisível. (╥﹏╥)

acontece quando processos/threads compartilham dados e pelo menos um deles escreve. se todos só leem, de boa. mas escrever sem coordenação é pedir pra dar problema.

EXEMPLO CLÁSSICO

dois processos incrementam uma variável compartilhada contador. o incremento não é atômico: é ler, somar 1, gravar. se ambos leem 10 ao mesmo tempo, ambos gravam 11. deveria ser 12. em sistemas reais, isso causa bugs silenciosos e terríveis. (T_T)

2. Região Crítica: O Banheiro dos Processos

a solução é criar uma região crítica (critical section): trecho de código que acessa recursos compartilhados. a regra é simples: se um processo tá na região crítica, nenhum outro pode entrar. é tipo banheiro público: se tá ocupado, vc espera. (。◕‿◕。)

pra isso funcionar direito, precisamos de três propriedades:

  • Exclusão mútua: só um processo na região crítica por vez.
  • Progresso: se ninguém tá na região crítica e alguém quer entrar, a decisão de quem entra não pode ser adiada eternamente.
  • Espera limitada: nenhum processo espera pra sempre. evita starvation.

existem várias formas de implementar isso. vamos às principais. (⌐■_■)

3. Semáforos: Sinalizador de Trânsito

o semáforo, inventado por Dijkstra (de novo ele!), é uma variável inteira que controla o acesso. duas operações atômicas:

  • P (ou wait/down): decrementa o semáforo. se ficar negativo, o processo dorme (bloqueia).
  • V (ou signal/up): incrementa o semáforo. se tinha processo dormindo, acorda um deles.

semáforo com valor 1 é chamado de semáforo binário ou mutex. ele garante exclusão mútua: só um processo entra na região crítica. semáforo com valor maior que 1 permite até N processos simultâneos. exemplo: banheiro com 3 cabines. (✿◠‿◠)

SEMAFORO NA PRÁTICA

em Java, java.util.concurrent.Semaphore. em C, sem_wait() e sem_post() das POSIX threads. em Python, threading.Semaphore. a ideia é sempre a mesma: controlar quantos podem passar. (•̀ᴗ•́)و

4. Mutex: O Cadeado Simples

Mutex (mutual exclusion) é parecido com um semáforo binário, mas com uma regra crucial: só quem travou pode destrancar (ownership). semáforo binário não exige isso. operações:

  • lock (acquire): tranca. se já tava travado, espera.
  • unlock (release): destranca. libera pra próximo.

é o mecanismo mais usado no mundo. toda vez que vc vê código multithreaded, tem mutex escondido por aí. mas tem um problema: se quem travou esquecer de destrancar, vira deadlock. ou se travar duas vezes sem destravar, trava também. cuidado!! (¬‿¬)

MUTEX RECURSIVO

alguns mutex permitem que a mesma thread trave várias vezes. chama recursive mutex ou reentrant lock. em Java é o ReentrantLock. útil quando uma função chama outra e ambas precisam do mesmo lock. mas se usar errado, vira spaghetti de locks. ( ̄ω ̄)

5. Monitores: Orientado a Objeto

Monitor é uma estrutura de alto nível que encapsula dados compartilhados e as operações de sincronização. a ideia é: o monitor garante que só um processo execute qualquer função dele por vez. é como se o próprio objeto tivesse um mutex embutido. (☆▽☆)

em linguagens como Java, todo objeto tem um monitor implícito. usar synchronized em um método ou bloco usa o monitor daquele objeto. em C#, lock faz o mesmo.

monitores também têm variáveis de condição: permitem que um processo espere por alguma condição dentro do monitor. exemplo: fila vazia, o consumidor dorme até o produtor acordar ele. isso é mais elegante que ficar testando em loop (busy waiting). (。♥‿♥。)

MONITOR VS SEMAFORO

semáforo é primitivo, baixo nível, poderoso, mas fácil de errar. monitor é mais estruturado, deixa o compilador gerar a sincronização, mas menos flexível. use semáforo quando precisar de controle fino. use monitor quando quiser algo mais organizado e menos propenso a bugs. (•̀ᴗ•́)و

6. Problemas Clássicos de Sincronização

existem problemas famosos que aparecem em toda prova de SO. bora ver. (ノ◕ヮ◕)ノ*:・゚✧

6.1 Produtor-Consumidor

um processo produz itens, outro consome. eles compartilham um buffer limitado. se o buffer tá cheio, o produtor espera. se tá vazio, o consumidor espera. precisa de sincronização dupla: mutex pro buffer e semáforos pra contar slots vazios/ocupados. (。•̀ᴗ-)✧

6.2 Jantar dos Filósofos

cinco filósofos sentam numa mesa circular. entre cada par de filósofos há um hashis. pra comer, um filósofo precisa dos dois hashis adjacentes. se todos pegarem o hashis da esquerda ao mesmo tempo, ninguém pega o da direita. deadlock. soluções: limitar quantos sentam (semáforo), pegar hashis em ordem diferente, ou usar monitor com fila. ( ͡~ ͜ʖ ͡°)

6.3 Leitores-Escritores

vários processos leem um banco de dados, outros escrevem. leitores podem acessar simultaneamente, mas escritores precisam acesso exclusivo. o desafio é evitar starvation: se leitores chegam sem parar, escritores nunca entram. precisa de regras justas. ( ͡~ ͜ʖ ͡°)

POR QUE ESTUDAR ISSO?

esses problemas não são só teoria. produtor-consumidor é fila de mensagens (RabbitMQ, Kafka). leitores-escritores é cache e banco de dados. jantar dos filósofos é qualquer sistema com recursos compartilhados limitados. entender os clássicos ajuda a resolver problemas reais. (。•̀ᴗ-)✧

7. Deadlock: O Abraço da Morte

Deadlock é quando processos ficam presos esperando uns pelos outros eternamente. imagina duas pessoas numa porta estreita: cada uma quer passar, mas nenhuma cede. ninguém anda. nunca. (T_T)

existem quatro condições necessárias pra deadlock acontecer (se uma falhar, não há deadlock):

  • Exclusão mútua: o recurso só pode ser usado por um processo por vez.
  • Posse e espera: o processo já tem um recurso e pede outro, sem liberar o primeiro.
  • Não preempção: recursos só podem ser liberados voluntariamente pelo processo.
  • Espera circular: existe um ciclo de processos esperando: A espera B, B espera C, C espera A.

estratégias pra lidar:

  • Prevenção: quebra uma das quatro condições. exemplo: exigir que processos peçam todos os recursos de uma vez (acaba com posse e espera). (¬‿¬)
  • Evitação: analisa alocações antes de conceder. o algoritmo do banqueiro (Dijkstra de novo!) simula se dar deadlock antes de liberar recurso. seguro, mas lento.
  • Detecção e recuperação: deixa acontecer, detecta (grafo de alocação de recursos) e mata processos ou tira recursos deles. brutal, mas funciona.
  • Ignorar: faz nada. se der deadlock, o usuário resolve (reinicia). é o que a maioria dos SOs fazem, incluindo Windows e Linux. sério. ( ̄▽ ̄)ノ
OSTRICH ALGORITHM

ignorar deadlock é tão comum que tem nome: Ostrich Algorithm (algoritmo da avestruz). a justificativa é que deadlock é raro, e prevenir/evitar tem custo alto. é mais barato deixar acontecer e reiniciar. clássico da engenharia: se consertar custa mais que deixar quebrar, deixa quebrar. (⌐■_■)

8. Starvation: Fome Eterna

Starvation é diferente de deadlock. em starvation, o processo poderia executar, mas nunca é escolhido. exemplo: fila de prioridade onde sempre chega alguém mais importante. o de baixa prioridade espera pra sempre. tá vivo, mas faminto. (╥﹏╥)

solução: envelhecimento (aging). quanto mais tempo esperando, mais a prioridade sobe. eventualmente, todo mundo é atendido. justiça computacional!! (◍•ᴗ•◍)

9. Considerações Finais

sincronização é um dos temas mais difíceis de SO, mas também um dos mais importantes. todo sistema concorrente — bancos de dados, servidores web, jogos, sistemas operacionais — precisa disso. sem sincronização, vira anarquia. com sincronização errada, vira deadlock. ( ͡~ ͜ʖ ͡°)

resumo rápido:

  • compartilha dados? precisa de região crítica.
  • região crítica? precisa de exclusão mútua.
  • exclusão mútua? use mutex, semáforo ou monitor.
  • locks múltiplos? cuidado com deadlock.
  • deadlock? previna, evite, detecte, ou ignore (ostrich style).
DICA DE DEV

se vc programa em Java, estude java.util.concurrent. tem Semaphore, ReentrantLock, CountDownLatch, CyclicBarrier, BlockingQueue... tudo pronto. reinventar sincronização é receita de dor de cabeça. use o que a linguagem oferece. (ノ◕ヮ◕)ノ*:・゚✧

fechou, senpai? (ノ◕ヮ◕)ノ*:・゚✧ até o próximo tema!! ٩(◕‿◕)۶

VÍDEOS RECOMENDADOS

UNIVESP — Aula 08: Comunicação entre Processos
UNIVESP — Aula 13: Problemas com Comunicação de Processos
UNIVESP — Aula 14: Deadlocks (Introdução)
UNIVESP — Aula 15: Tratamento de Deadlocks I
UNIVESP — Aula 16: Tratamento de Deadlocks II

LABORATÓRIO INTERATIVO

Simulador: Semáforo & Região Crítica

Controle um semáforo binário (mutex). Threads tentam entrar na região crítica. Se o semáforo está em 0, elas esperam.

1
SEMÁFORO
REGIÃO CRÍTICA (dentro)
FILA DE ESPERA (bloqueadas)
Semáforo em 1. Região crítica livre.
Simulador: Jantar dos Filósofos

Cinco filósofos, cinco hashis. Clique em um filósofo para tentar comer. Evite o deadlock!

Clique nos filósofos para pegar hashis e comer.
Visual: Condições de Deadlock
P1 P2 R1 R2 — posse (P tem R) - - requisição (P quer R) DEADLOCK: ciclo no grafo P1 tem R1 e quer R2 P2 tem R2 e quer R1
voltar aos temas da unidade 4