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

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.