Trilha 08 · Sistema operacional

Deadlock e sincronização

Quando múltiplos processos compartilham recursos, podem entrar em espera circular — nenhum avança, todos travados para sempre. Entender as condições de deadlock é o primeiro passo para preveni-lo.

① Intuição

A ponte de mão única

Imagine uma ponte estreita de mão única com carros dos dois lados. Um carro da direita entra, depois um da esquerda entra — agora estão frente a frente no meio da ponte. Nenhum pode recuar (sem preempção), cada um segura sua metade da ponte (hold and wait), cada metade só pode ser usada por um (exclusão mútua) e há espera circular. As 4 condições de Coffman estão presentes — é deadlock.

Deadlock é diferente de race condition: race condition (condição de corrida) é quando o resultado depende da ordem de execução de threads e pode estar errado — mas os processos continuam executando. Deadlock é quando nenhum processo pode avançar. Ambos são problemas de sincronização, mas têm causas e soluções diferentes.
② Visualização interativa

Grafo de alocação de recursos

Alterne entre os cenários para ver quando o grafo tem ciclo (deadlock) e quando está em estado seguro. O problema dos filósofos é um exemplo clássico de deadlock em espera circular.

P1 aguarda R1, P2 aguarda R2 — mas cada recurso está disponível para ser liberado.

GRAFO DE ALOCAÇÃO DE RECURSOS
P1P2P3R1R2R3ProcessosRecursos— alocado (R→P) ··· pedido (P→R)
✓ Estado seguro

Nenhum ciclo no grafo de alocação de recursos. P3 pode terminar primeiro (tem R3, não pede nada mais), libera R3 → P1 pode executar, libera R1 → P2 pode executar.

4 CONDIÇÕES DE COFFMAN (todas necessárias)
1. Exclusão mútua: Recurso só pode ser usado por 1 processo de cada vez.
2. Hold and wait: Processo segura ≥1 recurso enquanto espera por outro.
3. Não-preempção: Recurso só é liberado voluntariamente pelo processo.
4. Espera circular: Existe ciclo P1→R1→P2→R2→…→Pn→Rn→P1.
ESTRATÉGIAS DE SOLUÇÃO
③ Explicação técnica

Mutex, semáforos e seções críticas

// Mutex e semáforo — sincronização de threads

// Seção crítica protegida por mutex
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;

void* incrementa(void* arg) {
  for (int i = 0; i < 1000000; i++) {
    pthread_mutex_lock(&lock);    // bloqueia se já travado
    counter++;                    // seção crítica (1 thread por vez)
    pthread_mutex_unlock(&lock);  // libera
  }
}
// Sem o mutex: race condition! counter pode ser menor que 2.000.000
// (leitura-modificação-escrita não é atômica)

// Deadlock clássico com dois mutexes:
// Thread 1: lock(A) → lock(B)   ← já tem A, espera B
// Thread 2: lock(B) → lock(A)   ← já tem B, espera A
// → espera circular = deadlock

// Solução: sempre adquirir locks na mesma ordem
// Thread 1 e Thread 2: lock(A) primeiro, depois lock(B)
// → garante que nunca há ciclo

Operações atômicas e primitivas de sincronização

// Alternativas a mutex: operações atômicas

// x86-64 tem instruções atômicas nativas:
int val = 0;
__atomic_fetch_add(&val, 1, __ATOMIC_SEQ_CST);  // atomicamente: val++

// Em C++11:
std::atomic<int> counter(0);
counter.fetch_add(1);  // operação read-modify-write atômica
// Sem mutex! A CPU garante atomicidade via LOCK prefix na instrução
// ex: LOCK XADD [mem], reg — atomic add and fetch

// Quando usar o quê:
// atomic: contadores simples, flags, ponteiros (lock-free)
// mutex: proteção de estruturas complexas (mais de 1 campo)
// semáforo: limitar acessos simultâneos (ex: pool de conexões)
// condition variable: esperar por condição (produtor-consumidor)

// Liveness properties:
// Deadlock: nenhum processo avança
// Livelock: processos "executam" mas sem progresso (ex: dois que cedem ao mesmo tempo)
// Starvation: processo específico nunca recebe recurso que precisa
O problema dos filósofos (Dijkstra, 1965): 5 filósofos em uma mesa circular com 5 garfos. Para comer, cada um precisa de 2 garfos (esquerda e direita). Se todos pegarem o garfo da esquerda simultaneamente → deadlock. Solução clássica: um filósofo pega o garfo da direita antes do da esquerda (quebra a simetria). Outra: semáforo que permite no máximo 4 filósofos tentarem simultaneamente.
④ Projeto para programar

Implementando sincronização

Mini projeto: reproduza um deadlock em C com pthreads: duas threads, dois mutexes A e B. Thread 1 faz lock(A) → sleep(10ms) → lock(B). Thread 2 faz lock(B) → sleep(10ms) → lock(A). Use pthread_mutex_trylock para detectar o deadlock ao invés de travar para sempre.

Projeto principal: implemente o problema produtor-consumidor com buffer circular de tamanho N usando semáforos: sem_t empty (inicializado com N), sem_t full (inicializado com 0), mutex (para acesso ao buffer). Produtor: sem_wait(empty) → escreve → sem_post(full). Consumidor: sem_wait(full) → lê → sem_post(empty).

Desafio extra: implemente um leitor-escritor (readers-writers) com prioridade para escritores: múltiplos leitores podem ler simultaneamente, mas um escritor precisa de acesso exclusivo. Use pthread_rwlock_t ou implemente manualmente com mutex + condition variables. Teste com 10 threads leitoras e 2 escritoras e meça o throughput vs. a versão com mutex único.

⑤ Exercícios rápidos

Teste sua intuição

Quais são as 4 condições de Coffman para deadlock?
Qual é a diferença entre mutex e semáforo?
Qual é a diferença entre deadlock e livelock?
⑥ Aplicações no mundo real

Onde você encontra isso

🗄️

Deadlock em bancos de dados

Bancos de dados como PostgreSQL e MySQL detectam deadlocks automaticamente: monitoram o grafo de espera de locks de transação, e quando detectam ciclo, escolhem uma vítima (rollback de uma das transações). O usuário vê: "ERROR: deadlock detected — try your transaction again." Estratégia usual: retry automático na camada de aplicação.

🔒

Spinlocks no kernel Linux

Em código de kernel, mutexes não podem ser usados (podem dormir, e código de kernel com preempção desabilitada não pode dormir). Spinlocks são usados: a thread fica em loop apertado testando até o lock estar livre — desperdiça CPU mas tem latência mínima. Usados para seções críticas muito curtas (µs). Em multicore, são eficientes; em single-core, são inúteis.

⚛️

Algoritmos lock-free

Estruturas lock-free (como queues de Treiber ou Michael-Scott) usam operações atômicas como CAS (Compare-and-Swap) para garantir progresso sem mutexes. Nunca têm deadlock (sem locks!) mas são difíceis de implementar corretamente — o ABA problem, memory ordering e reclamação de memória tornam o código intrincado. Usados em engines de alta performance e runtimes de linguagem.

← Anterior: Sistema de arquivos Próxima: System calls →