O que é sincronização
O termo sincronização aparece o tempo todo em concorrência, e costuma ser usado de forma vaga. Vale fixar o que ele significa: sincronizar é impor alguma restrição sobre a ordem relativa em que as ações de threads diferentes acontecem.
Threads sem sincronização são independentes. O escalonador intercala as instruções delas de qualquer jeito, e nenhuma ordem entre threads diferentes é garantida. Sincronizar é justamente abrir mão dessa liberdade em pontos específicos, dizendo “isto aqui não pode acontecer antes daquilo”. Toda vez que você força uma ordem, está sincronizando, e está, de propósito, tornando o programa um pouco mais sequencial num ponto pra torná-lo correto.
Os problemas de sincronização se dividem em duas famílias, e é útil separá-las desde já porque elas pedem ferramentas diferentes.
Exclusão mútua é a família deste capítulo. A restrição é “estas ações não
podem se sobrepor”: enquanto uma thread está no counter++, nenhuma outra pode
estar. Não importa quem vai primeiro, importa que não seja ao mesmo tempo. A
ferramenta é o lock.
Coordenação é a outra família, e a restrição tem outra natureza: “esta ação só pode acontecer depois que aquela condição for verdadeira”. Uma thread que consome itens de uma fila precisa esperar até que exista algum item pra consumir. Uma thread que junta resultados precisa esperar até que todas as outras terminem. Aqui o problema não é impedir sobreposição, é fazer uma thread esperar por um acontecimento de outra. As ferramentas são outras (variáveis de condição, barreiras, semáforos usados como sinal), e são o assunto de um capítulo mais à frente.
A distinção não é meramente acadêmica. Usar a ferramenta errada é uma fonte comum de bugs: gente usando lock pra tentar fazer uma thread esperar por um evento acaba com busy-waiting desperdiçando CPU, e gente usando primitivas de coordenação pra proteger dados acaba sem a exclusão que precisava. Por ora, ficamos na primeira família. O lock resolve exclusão mútua, e é pra ele que vamos agora.