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

Producer-Consumer

Um lado gera itens de trabalho, o outro os processa, e entre eles há um buffer de tamanho fixo. Um servidor web coloca requisições numa fila e um pool de threads as atende. Um decodificador de vídeo enche um buffer de quadros que a placa de vídeo esvazia. O padrão é sempre o mesmo, e traz junto sempre os mesmos dois riscos: o produtor não pode escrever num buffer cheio, e o consumidor não pode ler de um buffer vazio. Se qualquer um dos dois ignorar isso, o programa corrompe dados ou lê lixo.

Contar quantas threads podem prosseguir é exatamente o que um semáforo faz, e aqui a gente precisa de dois. Um conta quantas posições livres o buffer ainda tem; o produtor faz P nele antes de escrever, e bloqueia se o buffer está cheio. O outro conta quantos itens prontos existem; o consumidor faz P nele antes de ler, e bloqueia se o buffer está vazio. Cada sem_post de um lado é o que eventualmente libera o outro.

Dois semáforos e um mutex

Se houvesse um só produtor e um só consumidor, os dois semáforos bastariam. Mas com vários produtores, dois deles podem passar pelo semáforo de posições livres ao mesmo tempo, e aí os dois vão escrever na mesma posição do buffer e avançar o mesmo índice, um por cima do outro. O semáforo garante que existe espaço; ele não garante que dois produtores não vão brigar pelo mesmo espaço. Essa briga pelo índice é uma seção crítica comum, e quem resolve seção crítica é o mutex.

Daí a divisão de trabalho que é o coração da solução: os semáforos coordenam (quantos podem entrar), o mutex protege (a manipulação dos índices). O Código 10-1 tem dois produtores e dois consumidores sobre um buffer de quatro posições:

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

#define TAM 4                 // tamanho do buffer
#define NP 2                  // produtores
#define NC 2                  // consumidores
#define POR_PROD 5            // itens que cada produtor gera
#define POR_CONS 5            // itens que cada consumidor consome

int buffer[TAM];
int in = 0, out = 0;

sem_t vazias;                 // posições livres
sem_t cheias;                 // itens prontos
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *produtor(void *arg) {
    int id = *(int *)arg;
    for (int i = 0; i < POR_PROD; i++) {
        int item = id * 100 + i;
        sem_wait(&vazias);            // espera uma posição livre
        pthread_mutex_lock(&mutex);   // protege os índices
        buffer[in] = item;
        in = (in + 1) % TAM;
        pthread_mutex_unlock(&mutex);
        sem_post(&cheias);            // agora tem mais um item pronto
    }
    return NULL;
}

void *consumidor(void *arg) {
    int id = *(int *)arg;
    for (int i = 0; i < POR_CONS; i++) {
        sem_wait(&cheias);            // espera um item pronto
        pthread_mutex_lock(&mutex);
        int item = buffer[out];
        out = (out + 1) % TAM;
        pthread_mutex_unlock(&mutex);
        sem_post(&vazias);            // liberou uma posição
        printf("consumidor %d pegou %d\n", id, item);
    }
    return NULL;
}

int main(void) {
    sem_init(&vazias, 0, TAM);        // começa com TAM posições livres
    sem_init(&cheias, 0, 0);          // e zero itens prontos
    pthread_t p[NP], c[NC];
    int pid[NP], cid[NC];
    for (int i = 0; i < NP; i++) { pid[i]=i; pthread_create(&p[i],NULL,produtor,&pid[i]); }
    for (int i = 0; i < NC; i++) { cid[i]=i; pthread_create(&c[i],NULL,consumidor,&cid[i]); }
    for (int i = 0; i < NP; i++) pthread_join(p[i],NULL);
    for (int i = 0; i < NC; i++) pthread_join(c[i],NULL);
    printf("pronto\n");
    return 0;
}

Código 10-1: Buffer circular com dois produtores e dois consumidores.

$ cc main.c -o main -pthread
$ ./main
consumidor 0 pegou 0
consumidor 1 pegou 1
consumidor 1 pegou 3
consumidor 1 pegou 4
consumidor 1 pegou 100
consumidor 0 pegou 2
consumidor 1 pegou 101
consumidor 0 pegou 102
consumidor 0 pegou 103
consumidor 0 pegou 104
pronto

Os dez itens saem: os do produtor 0 (0 a 4) e os do produtor 1 (100 a 104), cada um consumido uma única vez, sem nenhum sumir nem aparecer duplicado. A ordem de quem pega o quê muda a cada execução, porque o escalonador decide quando cada thread roda, mas as duas invariantes se mantêm sempre: nunca um produtor escreve num buffer cheio, nunca um consumidor lê de um vazio.

Repare como as três primitivas se encaixam. Tira o semáforo vazias e um produtor rápido passa por cima de itens que ninguém consumiu ainda. Tira o cheias e um consumidor lê posições que nunca foram escritas. Tira o mutex e dois produtores corrompem o índice in. Cada uma cobre um buraco que as outras duas não cobrem, e é essa complementaridade que faz a solução funcionar.

A questão do término

Uma sutileza fácil de passar batido: como o programa sabe parar? Aqui, cada produtor gera um número fixo de itens e cada consumidor consome um número fixo, com as contas batendo (dois produtores de cinco, dois consumidores de cinco). Cada thread sabe exatamente quantas voltas dar e termina sozinha.

Em sistemas de verdade, quem produz e quem consome raramente sabe o total de antemão. O consumidor fica num loop infinito esperando trabalho que pode nunca mais vir, e desligar isso com elegância, sem deixar consumidor preso pra sempre num sem_wait, é um problema à parte, com soluções próprias como enviar um valor especial que sinaliza “acabou”. O núcleo da coordenação, os dois semáforos contra o buffer e o mutex sobre os índices, é o que não muda.