Aula 09 — Exclusão Mútua: Abordagens e Algoritmos
Objetivos
Ao final desta aula você deve ser capaz de:
- Descrever e avaliar cada abordagem de exclusão mútua (desabilitar interrupções, variáveis lock simples, algoritmos de software, instruções atômicas, mutex).
- Demonstrar por que uma variável lock simples sem atomicidade não resolve o problema da seção crítica.
- Explicar a Solução de Peterson e verificar formalmente que ela satisfaz os três requisitos (exclusão mútua, progresso, espera limitada).
- Definir atomicidade e descrever as instruções
test_and_setecompare_and_swap. - Comparar busy waiting (spin) com bloqueio real, e indicar quando cada um é preferível.
- Identificar o requisito que
test_and_setsimples viola e descrever a solução com arraywaiting[].
Conteúdo
Panorama das abordagens
O problema da seção crítica pode ser abordado em diferentes níveis:
| Abordagem | Nível | Exclusão | Progresso | Espera Lim. | Overhead |
|---|---|---|---|---|---|
| Desabilitar int. | Hardware | ✓ (1 CPU) | ✓ | ✓ | Paralisa sistema |
| Var. lock simples | Software | ✗ | ✗ | ✗ | Spin ineficaz |
| Solução Peterson | Software | ✓ | ✓ | ✓ | Spin + restr. HW |
| TAS simples | Hardware/HW | ✓ | ✓ | ✗ | Spin |
| TAS + waiting[] | Hardware/HW | ✓ | ✓ | ✓ | Spin |
| CAS | Hardware/HW | ✓ | ✓ | ✓* | Spin/lock-free |
| Mutex (bloqueante) | SO/lib | ✓ | ✓ | ✓ | Context switch |
Abordagem 1: Desabilitar Interrupções
Em um sistema monoprocessador, a preempção só ocorre via interrupção. Se as interrupções forem desabilitadas durante a seção crítica, nenhuma troca de contexto pode ocorrer — exclusão mútua garantida.
/* Somente leitura — pseudocódigo conceitual */
cli(); /* disable interrupts (instrução privilegiada x86) */
/* seção crítica */
sti(); /* enable interrupts */
Análise dos três requisitos:
- Exclusão mútua: ✓ (em sistemas monoprocessador)
- Progresso: ✓
- Espera limitada: ✓ (se a SC for finita)
Por que não é suficiente em geral:
-
Instrução privilegiada:
cli/stisão instruções de modo kernel — programas de usuário não podem executá-las. O SO não pode confiar que aplicações as usem corretamente. -
Ineficaz em multiprocessador: desabilitar interrupções em uma CPU não impede que outra CPU acesse dados compartilhados simultaneamente. Seria necessário desabilitar em todas as CPUs via IPI (Inter-Processor Interrupt) — latência enorme.
-
Consequências de falha: se o processo travar dentro da SC com interrupções desabilitadas, o sistema congela completamente (sem timer, sem I/O, sem SO).
-
Esconde bugs de temporização: programas que dependem de interrupções (timers, drivers) podem falhar silenciosamente.
Uso legítimo: o kernel usa cli/sti (ou equivalentes) em seções extremamente curtas para proteger estruturas internas em sistemas monoprocessador ou para desabilitar interrupções localmente (um núcleo) em SMP — nunca como mecanismo geral de aplicações.
Abordagem 2: Variável Lock Simples (sem atomicidade)
A ideia intuitiva é usar uma variável compartilhada lock:
/* Somente leitura — tentativa ingênua INCORRETA */
int lock = 0; /* 0 = livre, 1 = ocupado */
void entrar_sc() {
while (lock == 1) ; /* espera */
lock = 1; /* adquire */
}
void sair_sc() {
lock = 0;
}
Por que não funciona:
Tempo P0 P1
T0 while(lock == 1): false
T1 — preempção! — while(lock == 1): false
T2 lock = 1 ← P1 adquire
T3 lock = 1 ← P0 também adquire!
T4 AMBOS NA SEÇÃO CRÍTICA ← violação de exclusão mútua
A verificação (while) e a atribuição (lock = 1) não são uma operação única e indivisível — o preempção pode ocorrer entre elas.
Diagnóstico: o problema é a ausência de atomicidade. Precisamos de uma operação que teste E modifique sem interrupção entre as duas etapas.
Abordagem 3: Solução de Peterson (revisão aprofundada)
A Solução de Peterson resolve o problema algoritmicamente para dois processos, sem instrução especial de hardware.
/* Somente leitura — Solução de Peterson para Pi e Pj */
int turn; /* de quem é a vez: i ou j */
bool flag[2]; /* flag[i] = Pi quer entrar na SC */
/* Código de Pi (Pj é simétrico com i e j trocados) */
do {
flag[i] = true; /* "quero entrar" */
turn = j; /* "cedo minha vez se houver conflito" */
while (flag[j] && turn == j)
; /* spin enquanto Pj quer E é a vez de Pj */
/* ─── SEÇÃO CRÍTICA ─── */
flag[i] = false; /* "terminei, pode entrar" */
/* ─── SEÇÃO REMANESCENTE ─── */
} while (true);
Prova formal dos três requisitos
1. Exclusão mútua: Pi entra na SC somente se flag[j] == false OU turn == i. Como turn só pode ser i ou j (não ambos), se Pi e Pj tentam entrar simultaneamente, turn será o último valor escrito — digamos turn == j (Pj cedeu). Então Pi vê turn == j → loop, Pj vê flag[i] == true && turn == j (falso, pois turn==j, não i?)
Detalhando: se turn == j, Pj executa while(flag[i] && turn == i) — turn é j, não i → turn == i é false → Pj sai do while e entra. Pi executa while(flag[j] && turn == j) — turn é j → turn == j é true e flag[j] é true → Pi permanece no while. ✓
2. Progresso: se nenhum está na SC (flag[j] == false), Pi entra imediatamente (loop: flag[j] && turn == j → false && ... → false). Se Pj está tentando entrar, turn resolverá o empate em favor de um deles. ✓
3. Espera limitada: Pi espera no máximo uma entrada de Pj. Quando Pj sai da SC, ele seta flag[j] = false ou turn = i, liberando Pi. ✓
Limitação em hardware moderno
Arquiteturas modernas (x86, ARM) reordenam instruções e usam
store buffers — uma escrita pode não ser imediatamente visível
por outro núcleo. A sequência:
flag[i] = true; ← pode ser atrasada no store buffer
turn = j; ← pode chegar ao outro núcleo antes
quebra a suposição de visibilidade imediata de Peterson.
Solução: adicionar barreiras de memória (memory fences):
flag[i] = true;
__sync_synchronize(); /* full memory barrier (GCC) */
turn = j;
Abordagem 4: Hardware de Sincronização — Instruções Atômicas
Processadores modernos fornecem instruções que realizam múltiplas operações em memória de forma indivisível — sem possibilidade de interrupção entre as etapas.
test_and_set (TAS)
/* Somente leitura — semântica atômica de test_and_set */
bool test_and_set(bool *alvo) {
bool rv = *alvo; /* lê valor atual */
*alvo = true; /* seta para true */
return rv; /* retorna o antigo */
/* TUDO ISSO É INDIVISÍVEL — hardware garante */
}
Implementação de EM com TAS (variável lock inicializada em false):
/* Somente leitura — exclusão mútua com TAS */
do {
while (test_and_set(&lock))
; /* spin: TAS retorna true enquanto lock=true */
/* ─── SEÇÃO CRÍTICA ─── */
lock = false; /* libera — setter de volta para false */
/* ─── seção remanescente ─── */
} while (true);
Análise:
- Exclusão mútua: ✓ — somente o processo que encontrou
lock = falseobtém o lock (TAS atômico garante que apenas um vê false). - Progresso: ✓
- Espera limitada: ✗ — um processo pode ser "passado na frente" indefinidamente.
TAS com espera limitada — array waiting[]
Para garantir espera limitada, usamos um array waiting[] para rastrear quem está esperando:
/* Somente leitura — TAS com espera limitada */
bool waiting[n]; /* waiting[i] = true: Pi quer entrar */
bool lock; /* inicializado em false */
/* Código de Pi */
do {
waiting[i] = true;
bool key = true;
while (waiting[i] && key)
key = test_and_set(&lock); /* tenta adquirir */
waiting[i] = false;
/* ─── SEÇÃO CRÍTICA ─── */
/* ao sair: procura quem está esperando (ordem cíclica) */
j = (i + 1) % n;
while ((j != i) && !waiting[j])
j = (j + 1) % n;
if (j == i) /* ninguém esperando — libera lock */
lock = false;
else /* acorda o próximo na fila cíclica */
waiting[j] = false;
/* ─── seção remanescente ─── */
} while (true);
Por que garante espera limitada: ao sair da SC, Pi percorre o array waiting ciclicamente e acorda o primeiro processo esperando. Qualquer processo esperará no máximo n-1 entradas antes de ser atendido.
compare_and_swap (CAS)
/* Somente leitura — semântica atômica de compare_and_swap */
int compare_and_swap(int *val, int esperado, int novo) {
int temp = *val;
if (*val == esperado)
*val = novo;
return temp; /* retorna o valor ANTERIOR */
/* TUDO ATÔMICO */
}
Uso para EM (lock inicializado em 0):
/* Somente leitura */
do {
while (compare_and_swap(&lock, 0, 1) != 0)
; /* spin enquanto lock != 0 */
/* ─── SEÇÃO CRÍTICA ─── */
lock = 0;
/* ─── seção remanescente ─── */
} while (true);
CAS é mais poderoso que TAS: permite implementar estruturas lock-free (sem locks) — filas, pilhas, contadores — que não bloqueiam nenhum thread. Na x86-64, CAS é implementado como CMPXCHG (atomicamente com prefixo LOCK).
Atomicidade em C moderno — <stdatomic.h>
/* Executável — contador atômico sem mutex (C11) */
/* Compile: gcc -o atomic_demo atomic_demo.c -lpthread */
#include <stdatomic.h>
#include <pthread.h>
#include <stdio.h>
#define N_THREADS 8
#define ITER 100000
atomic_long contador = 0; /* atomic_long: operações são atômicas */
void *incrementa(void *arg) {
for (int i = 0; i < ITER; i++)
atomic_fetch_add(&contador, 1); /* equivale a CAS internamente */
return NULL;
}
int main(void) {
pthread_t tids[N_THREADS];
for (int i = 0; i < N_THREADS; i++)
pthread_create(&tids[i], NULL, incrementa, NULL);
for (int i = 0; i < N_THREADS; i++)
pthread_join(tids[i], NULL);
printf("Esperado: %ld | Obtido: %ld\n",
(long)N_THREADS * ITER, (long)contador);
/* Sempre correto, sem mutex, com menor overhead */
return 0;
}
Abordagem 5: Lock Mutex (revisão com foco em comparação)
O mutex combina a atomicidade do TAS/CAS com bloqueio real — em vez de spin, o processo é suspenso e acordado quando o lock fica disponível.
TAS spinlock: Mutex bloqueante:
while(TAS(&lock)) ; acquire():
// CPU 100% girando if (!available) → bloqueia (sleep)
// Útil se espera < 1µs available = false
release():
available = true
wakeup(próximo na fila)
Quando usar cada um:
| Situação | Recomendação | Razão |
|---|---|---|
| Seção crítica < 1µs, multicore | Spinlock | Evita overhead de context switch |
| Seção crítica > 1µs | Mutex bloqueante | Evita desperdício de CPU |
| Sistema monoprocessador | Mutex bloqueante | Spin nunca libera CPU para quem tem o lock |
| Kernel Linux (regiões curtíssimas) | Spinlock (spin_lock) | Não pode dormir em contexto de interrupção |
| Aplicações de usuário | Mutex (pthread_mutex_t) | Correto e eficiente para seções variáveis |
Visão integrada: cronologia das soluções
Problema:
Variável compartilhada → race condition → seção crítica
Solução simplista (errada):
Variável lock sem atomicidade → não funciona
Solução algorítmica (correta, limitada):
Peterson → funciona em teoria, pode falhar em hardware moderno
Solução de hardware (correta, com spin):
TAS/CAS → correto, mas busy waiting (CPU desperdiçada)
TAS + waiting[] → correto e com espera limitada
Solução de SO (correta, eficiente para longas esperas):
Mutex bloqueante → correto, sem spin, com context switch
Operações atômicas modernas:
atomic_t, CAS → lock-free, para contadores e estruturas simples
Exercícios
Questões dissertativas
Por que desabilitar interrupções não é uma solução viável para exclusão mútua em sistemas multiprocessador? Explique em termos de hardware o que precisaria ser feito e por que isso é impraticável.
Demonstre com um trace de execução específico por que a variável lock simples (sem atomicidade) não garante exclusão mútua.
Explique a intuição por trás de cada uma das duas variáveis na Solução de Peterson (turn e flag[]). Por que usar apenas uma delas não resolve o problema?
Defina atomicidade no contexto de operações de memória. Por que test_and_set deve ser implementada em hardware como instrução atômica?
Por que a implementação simples de TAS não garante espera limitada? Descreva o cenário em que um processo pode esperar indefinidamente.
Explique a diferença semântica entre test_and_set e compare_and_swap. Em que situação CAS é mais poderoso que TAS?
Quando é mais eficiente usar um spinlock do que um mutex bloqueante? Justifique em termos de overhead de cada abordagem.
Considere a versão de TAS com o array waiting[]. Prove informalmente que ela satisfaz espera limitada.
Explique o conceito de memory fence (barreira de memória). Por que ela é necessária para que a Solução de Peterson funcione em hardware moderno?
Compare as cinco abordagens de exclusão mútua desta aula numa tabela, avaliando: corretude (3 requisitos), tipo de espera, dependência de hardware, e aplicabilidade prática.
Quiz de múltipla escolha
1. Por que a variável lock simples (while(lock==1); lock=1;) não resolve o problema da seção crítica?
- a)Porque o compilador pode otimizar o loop e removê-lo
- b)Porque a verificação e a atribuição são operações separadas — o processo pode ser preemptado entre elas, permitindo que dois processos entrem na SC
- c)Porque variáveis booleanas não podem ser compartilhadas entre processos
- d)Porque o sistema operacional não permite acesso direto a variáveis globais
- e)Porque a instrução while não é suportada em modo kernel
2. Na Solução de Peterson, Pi seta turn=j (e não turn=i) antes de esperar. Qual o propósito dessa atribuição?
- a)Indica que Pi está executando sua seção crítica
- b)Sinaliza que Pi terminou e liberou a seção crítica
- c)Cede a vez para Pj em caso de conflito simultâneo — quem seta turn por último 'perdeu' o empate
- d)Inicializa a variável turn para um valor neutro
- e)Incrementa um contador de quantas vezes Pi tentou entrar
3. test_and_set(&lock) é chamado por dois processos simultaneamente em CPUs diferentes. O que a atomicidade do hardware garante?
- a)Ambos retornam false e ambos obtêm o lock
- b)Exatamente um vê false (obtém o lock); o outro vê true (não obtém)
- c)O resultado depende qual CPU é mais rápida
- d)Ambos bloqueiam até que um deles libere o lock manualmente
- e)Um deles recebe uma exceção de violação de acesso
4. compare_and_swap(val, 5, 10) é chamado. *val = 7 no momento da chamada. O que retorna e *val é alterado?
- a)Retorna 5, *val = 10
- b)Retorna 7, *val = 10
- c)Retorna 7, *val permanece 7
- d)Retorna 10, *val = 5
- e)Retorna 0 (falha)
5. Por que TAS simples viola o requisito de espera limitada?
- a)Porque TAS pode retornar valores incorretos sob alta carga
- b)Porque ao liberar o lock (lock=false), qualquer processo em spin pode obtê-lo — sem garantia de ordem, um processo pode ser 'pulado' indefinidamente
- c)Porque TAS não funciona em sistemas com mais de 2 processos
- d)Porque TAS não garante exclusão mútua — dois processos podem entrar simultaneamente
- e)Porque o processo que detém o lock nunca libera voluntariamente
6. Em que situação o spinlock é MAIS eficiente que o mutex bloqueante?
- a)Quando a seção crítica é longa (> 100ms)
- b)Em sistemas monoprocessador com apenas 1 núcleo
- c)Quando a espera esperada é muito curta (< tempo de 2 context switches, ~1-2µs em hardware moderno)
- d)Quando há muitos processos competindo pelo mesmo lock
- e)Nunca — o mutex bloqueante é sempre mais eficiente
7. A Solução de Peterson pode falhar em hardware moderno sem barreiras de memória. Qual mecanismo de hardware causa esse problema?
- a)Cache L1 — pode armazenar valores incorretos
- b)Store buffer — escritas podem ser adiadas e vistas em ordem diferente por outros núcleos
- c)Branch predictor — pode executar o código errado
- d)Out-of-order execution — pode reordenar leituras de memória
- e)Pipeline stall — impede que escritas sejam completadas a tempo
8. atomic_fetch_add(&x, 1) em C11 é equivalente a qual operação?
- a)x++ sem garantias de atomicidade
- b)Uma implementação de x++ usando CAS ou instrução atômica de hardware, garantindo que o incremento é indivisível
- c)Um mutex que protege x++
- d)Uma operação que incrementa x apenas se x == 0
- e)Uma macro que desabilita interrupções antes do incremento
9. Qual dos três requisitos da seção crítica é MAIS difícil de satisfazer com variáveis de turno simples (onde turn alterna P0/P1/P0/P1)?
- a)Exclusão mútua — dois processos podem entrar ao mesmo tempo
- b)Progresso — P0 pode estar impedido de entrar mesmo que P1 não queira a SC
- c)Espera limitada — P0 pode esperar indefinidamente
- d)Atomicidade — a variável turn não pode ser lida atomicamente
- e)Corretude — o resultado depende da velocidade dos processos
10. Desabilitar interrupções é uma solução viável para seções críticas dentro do KERNEL Linux. Por quê?
- a)Porque o kernel nunca usa multiprocessador
- b)Porque o kernel usa apenas um thread e não precisa de exclusão mútua
- c)Porque seções curtíssimas de kernel podem desabilitar interrupções localmente em um núcleo para proteger estruturas de dados acessadas apenas por aquele núcleo
- d)Porque o kernel tem permissão para monopolizar a CPU indefinidamente
- e)Porque o kernel usa memória ROM que não pode ser modificada por outros processos
Referências
Principais (essenciais)
- SILBERSCHATZ, A.; GALVIN, P. B.; GAGNE, G. Fundamentos de Sistemas Operacionais. 9. ed. LTC, 2015.
- §5.1 — Antecedentes (race condition, counter++)
- §5.2 — O Problema da Seção Crítica (3 requisitos)
- §5.3 — Solução de Peterson
- §5.4 — Hardware de Sincronização (TAS, CAS, waiting[])
- §5.5 — Locks Mutex
Aprofundamento (opcionais)
- TANENBAUM, A. S. Sistemas Operacionais Modernos. 4. ed. Pearson, 2016. §2.3.3–2.3.5.
- HERLIHY, M.; SHAVIT, N. The Art of Multiprocessor Programming. Morgan Kaufmann, 2012. Cap. 2.
- Linux kernel source —
arch/x86/include/asm/atomic.h(implementação de operações atômicas x86).
Recursos Complementares
Opcional — Vídeos para reforçar os conceitos desta aula.
21:00Critical Section Problem — Process Synchronization
Neso Academy
Condição de corrida, seção crítica e os três requisitos. Cobre §5.1 e §5.2 do Silberschatz.
25:00:00Operating Systems — Full Course (25h)
freeCodeCamp.org
Para esta aula, foque nos módulos Critical Section Problem, Peterson's Solution e Hardware Synchronization.