Semáforos
Um mutex deixa uma thread por vez na seção crítica. E se o recurso protegido comportasse três ao mesmo tempo, ou dez? Um pool com três conexões de banco pode atender três threads simultâneas; a quarta precisa esperar uma das três liberar. Um mutex não sabe expressar isso, porque ele só conta até um: ocupado ou livre.
O semáforo é essa generalização. Ele guarda um contador inteiro não-negativo e oferece duas operações sobre ele. Uma decrementa o contador; se ele já está em zero, a thread bloqueia até que dê pra decrementar. A outra incrementa o contador, possivelmente acordando alguma thread que esperava. Dijkstra, que inventou a primitiva, chamou as duas de P (a que decrementa e pode esperar) e V (a que incrementa e libera). O contador representa quantas unidades do recurso ainda estão disponíveis.
Com o contador inicializado em um, você tem de volta o mutex: a primeira thread decrementa pra zero e entra, qualquer outra encontra zero e espera, a primeira incrementa de volta pra um ao sair. Esse caso é o semáforo binário. Inicializado em N, ele vira um semáforo contador que deixa N threads passarem antes de começar a bloquear.
Um estacionamento com N vagas
O Código 9-2 modela um estacionamento de três
vagas com oito carros chegando ao mesmo tempo. O semáforo vagas começa em três, e
cada carro faz P pra entrar e V pra sair:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
sem_t vagas;
void *carro(void *arg) {
int id = *(int *)arg;
sem_wait(&vagas); // P: pega uma vaga, ou espera se não tem
printf("carro %d estacionou\n", id);
usleep(100000); // fica um tempo estacionado
printf("carro %d saiu\n", id);
sem_post(&vagas); // V: libera a vaga
return NULL;
}
int main(void) {
sem_init(&vagas, 0, 3); // 3 vagas; o 0 do meio: compartilhado só entre threads
pthread_t t[8]; int id[8];
for (int i = 0; i < 8; i++) { id[i] = i; pthread_create(&t[i], NULL, carro, &id[i]); }
for (int i = 0; i < 8; i++) pthread_join(t[i], NULL);
sem_destroy(&vagas);
return 0;
}
Código 9-2: Três vagas, oito carros, coordenados por um semáforo.
$ cc main.c -o main -pthread
$ ./main
carro 0 estacionou
carro 2 estacionou
carro 1 estacionou
carro 2 saiu
carro 0 saiu
carro 1 saiu
carro 3 estacionou
carro 5 estacionou
carro 4 estacionou
...
Em nenhum momento existem mais de três carros estacionados ao mesmo tempo. Assim que
um sai e faz sem_post, o contador sobe pra um, e uma das threads paradas no
sem_wait acorda, decrementa de volta pra zero, e entra. A sem_wait é o P e a
sem_post é o V; o sem_init recebe o valor inicial do contador no terceiro
argumento, e o segundo argumento, aqui 0, diz que o semáforo é compartilhado só
entre threads do mesmo processo.
Semáforo binário não é mutex
É tentador olhar pro semáforo binário e concluir que ele é só um mutex com outro
nome. A diferença está em quem pode liberar. Um mutex tem dono: a thread que
adquire é a única que pode liberar, e várias implementações checam isso e recusam um
unlock vindo de outra thread. O semáforo não tem dono. Qualquer thread pode chamar
sem_post, inclusive uma que nunca chamou sem_wait.
Essa ausência de dono é exatamente o que torna o semáforo uma ferramenta de
coordenação, e não só de exclusão. Uma
thread pode ficar bloqueada num sem_wait esperando um sinal, e uma outra thread,
quando um evento acontece, faz sem_post pra liberá-la. Um faz o papel de “espere
até isto ficar pronto”, o outro de “pronto, pode ir”. Não é uma thread protegendo
seus próprios dados; é uma thread avisando a outra. O mutex não consegue fazer isso,
porque o unlock vindo de quem não é o dono não é permitido.
O preço dessa flexibilidade é que semáforo é fácil de errar. Um sem_post a mais e o
contador passa do teto que você imaginava, deixando entrar mais threads que o recurso
suporta. Um sem_post a menos e uma vaga some pra sempre, até o programa travar por
falta de vagas que existem mas ninguém devolveu. O mutex, ao amarrar liberar a quem
adquiriu, elimina essa classe de erro inteira. Por isso, quando o que você quer é
mesmo exclusão mútua simples, o mutex é mais seguro; o semáforo brilha quando o
problema é contar recursos ou sinalizar entre threads.