Introdução

Nesta aula de número 215 da disciplina de Ciências da Computação, abordamos um dos tópicos mais importantes em sistemas operacionais: a sincronização de processos. Quando múltiplos processos ou threads executam concorrentemente e compartilham dados, é necessário coordenar suas execuções para evitar resultados inconsistentes. Vamos estudar os conceitos de seção crítica, condições de corrida, soluções de software e hardware, semáforos e monitores.

Seção Crítica e Condição de Corrida

Uma condição de corrida (race condition) ocorre quando o resultado de um programa depende da ordem de execução de processos concorrentes. O trecho de código onde ocorre acesso a recursos compartilhados é chamado de seção crítica. Para resolver o problema, devemos garantir que apenas um processo por vez execute sua seção crítica. As condições necessárias para uma boa solução são:

  • Exclusão mútua: se um processo executa sua seção crítica, nenhum outro pode executar a sua.
  • Progresso: se nenhum processo está na seção crítica, a decisão sobre quem entra não pode ser adiada indefinidamente.
  • Espera limitada: um processo que deseja entrar na seção crítica não pode esperar para sempre.

Soluções para Exclusão Mútua

Algoritmo de Peterson

Uma solução clássica em software para dois processos utiliza duas variáveis compartilhadas: turn (indica de quem é a vez) e flag[] (indica se o processo deseja entrar). O processo i executa:

flag[i] = true;
turn = j;
while (flag[j] && turn == j);
// seção crítica
flag[i] = false;

Essa solução satisfaz exclusão mútua, progresso e espera limitada.

Soluções de Hardware

Muitas arquiteturas oferecem instruções especiais como TestAndSet e Swap. A instrução TestAndSet(valor) retorna o valor antigo e atribui true atomicamente. Usando uma variável global lock, conseguimos exclusão mútua com spinlock:

while (TestAndSet(&lock));
// seção crítica
lock = false;

Essas soluções são simples, mas podem desperdiçar CPU em espera ocupada.

Semáforos

Propostos por Dijkstra, semáforos são variáveis inteiras gerenciadas por duas operações atômicas: wait (também chamada P) e signal (ou V). Um semáforo S pode ser binário (0 ou 1) ou contador (qualquer valor inteiro).

wait(S):
  while (S <= 0);
  S--;

signal(S):
  S++;

Semáforos resolvem o problema da seção crítica de forma mais estruturada. Um semáforo binário atua como mutex. Semáforos contadores são usados para sincronizar acesso a um número finito de recursos.

Exemplo: Sincronização entre processos

Para garantir que o processo B execute apenas após A, podemos usar um semáforo sem inicializado em 0. O processo A executa signal(sem) ao final, e o processo B executa wait(sem) no início.

Problemas Clássicos de Sincronização

Produtor‑Consumidor

Um ou mais produtores inserem itens em um buffer compartilhado, e consumidores os removem. O buffer tem tamanho limitado. Usamos três semáforos: empty (inicia com N), full (0) e mutex (1). O produtor faz wait(empty); wait(mutex); insere, signal(mutex); signal(full);. O consumidor faz simétrico. Esse padrão aparece em sistemas de mensagens e filas.

Leitores‑Escritores

Vários leitores podem ler ao mesmo tempo, mas escritores precisam de acesso exclusivo. Uma solução com semáforos permite que leitores entrem se nenhum escritor estiver ativo, usando um contador de leitores. Esse problema é fundamental em bancos de dados e sistemas de arquivos.

Jantar dos Filósofos

Cinco filósofos alternam entre pensar e comer. Para comer, precisam de dois garfos (recursos compartilhados). Esta situação pode levar a deadlock se todos pegarem o garfo esquerdo simultaneamente. Soluções incluem limitar o número de filósofos na mesa ou usar semáforos com ordenação cuidadosa.

Monitores

Monitores são uma abstração de alto nível que encapsula variáveis, funções e sincronização em uma única estrutura. Apenas um processo pode estar ativo dentro do monitor por vez. Linguagens como Java implementam monitores com a palavra‑chave synchronized e os métodos wait(), notify() e notifyAll(). Monitores eliminam a necessidade de gerenciar manualmente semáforos e reduzem a chance de erros.

class Buffer {
  synchronized void put(int item) { ... }
  synchronized int get() { ... }
}

Variáveis de condição dentro do monitor permitem esperar por uma condição específica, liberando temporariamente o monitor.

Perguntas Frequentes

O que é uma condição de corrida?

É uma situação em que vários processos acessam e manipulam dados compartilhados simultaneamente, e o resultado final depende da ordem de execução, podendo levar a inconsistências.

Qual a diferença entre semáforo e mutex?

Um mutex é um semáforo binário que garante exclusão mútua, mas também pode incluir conceitos de propriedade (quem o tranca deve destrancar). Um semáforo contador permite gerenciar múltiplas unidades de recurso, não tem conceito de proprietário e pode ser usado para sincronização entre processos não relacionados.

O que é deadlock?

Deadlock ocorre quando cada processo espera por um recurso que está retido por outro processo, formando um ciclo de espera que impede qualquer progresso. É um risco real em qualquer sistema com exclusão mútua e espera por múltiplos recursos.

Conclusão

A sincronização de processos é fundamental para a construção de sistemas operacionais confiáveis e eficientes. Desde soluções clássicas como o algoritmo de Peterson até abstrações modernas como monitores, os mecanismos vistos nesta aula formam a base para o desenvolvimento de software concorrente. Na próxima aula, continuaremos explorando tópicos avançados de gerenciamento de processos.