O que é um Lock
Um lock (ou “tranca”) é um objeto com duas operações: adquirir (acquire/lock) e liberar (release/unlock). A regra é simples: no máximo uma thread pode ter o lock adquirido por vez. Se uma thread tenta adquirir um lock que já está com outra, ela bloqueia, ou seja, para de executar e fica esperando, até que a dona atual libere.
Com isso, o protocolo de entrada e saída da seção anterior vira duas chamadas:
adquirir(lock) // protocolo de entrada
// trecho protegido
liberar(lock) // protocolo de saída
Quem chega primeiro e adquire o lock entra no trecho. Qualquer outra que chegar
enquanto isso fica bloqueada no adquirir. Quando a primeira chama liberar, uma
das que estavam esperando é acordada, adquire o lock, e entra. Uma de cada vez,
que é exatamente a exclusão mútua que queríamos.
O que faz o adquirir ser atômico
Na seção anterior vimos que tentar fazer isso na unha com uma flag ocupado
não funciona, porque while (ocupado) e ocupado = 1 são duas operações e outra
thread pode se meter entre elas. O lock resolve isso porque o adquirir usa uma
instrução do próprio processador que faz “testar e marcar” de forma indivisível.
O exemplo clássico é o test-and-set: uma instrução que, num único passo que o hardware garante não ser interrompível, lê o valor atual de uma posição de memória e escreve 1 nela, devolvendo o valor que estava lá antes. Em pseudocódigo, o efeito é:
// tudo isto acontece como uma operação atômica, indivisível:
int test_and_set(int *alvo) {
int antigo = *alvo;
*alvo = 1;
return antigo;
}
A diferença pro entrar quebrado da flag é que aqui não tem janela entre o teste
e a marcação. Duas threads não conseguem, ambas, ver 0 e marcar 1: a primeira que
executar o test_and_set recebe 0 de volta (e entra), e a segunda recebe 1 (e
sabe que precisa esperar). O hardware serializa as duas execuções da instrução.
Processadores modernos oferecem primitivas assim (xchg, cmpxchg no x86, ou
pares como ldrex/strex no ARM), e é sobre elas que os locks são construídos.
Bloquear é diferente de girar
Tem uma decisão embutida no “fica esperando” do adquirir. O que a thread faz
enquanto o lock está ocupado? Duas respostas possíveis:
- Girar (spin): ficar num loop testando o lock repetidamente, queimando CPU até ele liberar.
- Dormir: avisar o sistema operacional “me tire de execução até este lock liberar”, cedendo o core pra outra thread rodar.
A escolha entre girar e dormir tem consequências grandes de performance, e cada uma tem seu lugar. Girar desperdiça CPU enquanto espera, mas evita o custo de tirar e recolocar a thread em execução; vale a pena quando a espera é curtíssima. Dormir tem o custo fixo de envolver o sistema operacional, mas libera o core pra trabalho útil; vale a pena quando a espera pode ser longa. O lock que gira é o spinlock, e o lock que dorme, o tipo mais usado no dia a dia, é o mutex.