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

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.