Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Critical Section

Nos dois problemas anteriores, existia sempre um trecho de código que não podia ter duas threads dentro dele ao mesmo tempo. No contador, era o counter++. No saque, era o par checa-então-debita. Esse trecho tem um nome: seção crítica (critical section). É a parte do código que acessa um recurso compartilhado e que precisa rodar sem interferência de outras threads pra manter o programa correto.

O termo é de Dijkstra, dos anos 60, e já apareceu aqui de forma prática. Quando protegemos o counter++ com um mutex, a seção crítica era exatamente o trecho entre o lock e o unlock. As três propriedades que uma solução de exclusão mútua precisa satisfazer, exclusão mútua, progresso e espera limitada, foram enunciadas em torno justamente desse conceito de seção crítica, e estão no capítulo sobre o problema da exclusão mútua. O que falta aqui não é repetir aquilo, é desenvolver a habilidade de reconhecer uma seção crítica no seu próprio código, antes de sair aplicando mutex em tudo.

A janela de leitura-modificação-escrita

O caso mais comum de seção crítica é a sequência read-modify-write: uma thread lê um estado compartilhado, calcula algo a partir dele, e escreve o resultado de volta. O counter++ é o exemplo mínimo. Uma única linha, mas três instruções: lê counter num registrador, incrementa o registrador, escreve de volta.

O perigo mora na janela entre a leitura e a escrita. Enquanto uma thread está com o valor no registrador, incrementando, o counter na memória ainda tem o valor antigo. Se outra thread ler counter nesse intervalo, ela lê o valor desatualizado, e as duas acabam gravando o mesmo resultado. É por isso que a sequência inteira precisa se comportar como uma operação indivisível, do começo da leitura até o fim da escrita. Exclusão mútua sobre essa janela é o que transforma o 809976 de volta em 1000000.

O saque do capítulo sobre race condition é a mesma janela, só que mais espalhada: lê o saldo (if conta.saldo() >= valor), decide (passou no teste), escreve (conta.debita(valor)). Entre a leitura e a escrita, o saldo pode ter mudado. Que a leitura e a escrita estejam em linhas diferentes, ou até em funções diferentes, não muda a natureza do problema.

Reconhecendo seções críticas no seu código

O padrão a procurar é sempre o mesmo: uma decisão ou um cálculo baseado num estado compartilhado, seguido de uma modificação desse estado, com a suposição implícita de que o estado não mudou no meio do caminho. Sempre que você vir esse formato, tem uma seção crítica ali, e ela precisa rodar de forma atômica em relação às outras threads.

Reconhecer essas janelas é o que separa quem protege o programa certo de quem sai espalhando lock aleatoriamente (o que, além de não garantir corretude, ainda mata a performance, porque cada lock desnecessário serializa threads que poderiam rodar em paralelo). O olho pra identificar seções críticas vem com prática, e é uma habilidade que vale mais do que decorar qual primitiva usar: a primitiva você escolhe depois de saber o que precisa proteger, nunca antes.