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

Dining Philosophers

Cinco filósofos sentam numa mesa redonda. Entre cada par de vizinhos há um garfo, cinco garfos no total, e no centro há um prato de espaguete que exige dois garfos pra comer. Cada filósofo alterna entre pensar e comer. Pra comer, ele pega o garfo da esquerda, depois o da direita, come, e devolve os dois. O problema, formulado por Dijkstra, é achar um jeito de todos comerem sem que ninguém fique preso.

A solução ingênua é escrever exatamente o enunciado: pega o garfo da esquerda, pega o da direita, come. Cada garfo é um mutex, porque um garfo só serve a um filósofo por vez.

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

#define N 5
pthread_mutex_t garfo[N];

void *filosofo(void *arg) {
    int id = *(int *)arg;
    int esq = id, dir = (id + 1) % N;
    for (;;) {
        pthread_mutex_lock(&garfo[esq]);      // pega o garfo da esquerda
        printf("filosofo %d pegou garfo esquerdo\n", id);
        usleep(100000);                        // hesita um instante
        pthread_mutex_lock(&garfo[dir]);      // quer o da direita
        printf("filosofo %d comeu\n", id);
        pthread_mutex_unlock(&garfo[dir]);
        pthread_mutex_unlock(&garfo[esq]);
    }
    return NULL;
}

int main(void) {
    setbuf(stdout, NULL);                      // mostra a saída antes do travamento
    pthread_t t[N]; int id[N];
    for (int i = 0; i < N; i++) pthread_mutex_init(&garfo[i], NULL);
    for (int i = 0; i < N; i++) { id[i]=i; pthread_create(&t[i],NULL,filosofo,&id[i]); }
    for (int i = 0; i < N; i++) pthread_join(t[i],NULL);
    return 0;
}

Código 10-3: A solução ingênua, que trava.

O usleep entre pegar um garfo e o outro cumpre o mesmo papel de sempre: dá tempo pra todos pegarem o primeiro garfo antes de qualquer um tentar o segundo. Rodando:

$ cc main.c -o main -pthread
$ ./main
filosofo 0 pegou garfo esquerdo
filosofo 1 pegou garfo esquerdo
filosofo 2 pegou garfo esquerdo
filosofo 4 pegou garfo esquerdo
filosofo 3 pegou garfo esquerdo
^C
$

Nenhum “comeu” aparece. Os cinco pegaram o garfo da esquerda, e agora cada um quer o garfo da direita, que é o garfo esquerdo do vizinho, que o vizinho está segurando e não vai soltar porque ele também está esperando o dele. É um deadlock, e das quatro condições que o produzem, a que salta aos olhos aqui é a espera circular: o filósofo 0 espera o 1, que espera o 2, e assim por diante até fechar o anel.

Duas saídas

A primeira saída já conhecemos do deadlock: quebrar a espera circular impondo uma ordem global na aquisição. Se, em vez de “cada um pega o seu garfo esquerdo primeiro”, a regra for “todos pegam primeiro o garfo de menor número”, o anel não se fecha. O filósofo 4, que pegaria o garfo 4 e depois o 0, passa a pegar o 0 e depois o 4, e essa única inversão basta pra romper o ciclo.

A segunda saída é mais interessante porque usa a ferramenta deste capítulo e ataca o problema por outro ângulo: em vez de mudar a ordem, limitar quantos tentam comer ao mesmo tempo. O deadlock só é possível porque os cinco tentam sentar pra comer de uma vez. Se no máximo quatro tentarem, sempre sobra um garfo pra alguém completar o par, comer, e liberar os seus. Basta um porteiro que admita no máximo quatro filósofos à mesa, e um porteiro que conta é um semáforo:

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

#define N 5
pthread_mutex_t garfo[N];
sem_t garcom;                          // admite no máximo N-1 à mesa
int refeicoes[N] = {0};

void *filosofo(void *arg) {
    int id = *(int *)arg;
    int esq = id, dir = (id + 1) % N;
    for (int r = 0; r < 3; r++) {      // cada um come três vezes
        sem_wait(&garcom);             // pede lugar ao garçom
        pthread_mutex_lock(&garfo[esq]);
        pthread_mutex_lock(&garfo[dir]);
        refeicoes[id]++;
        printf("filosofo %d comeu (%d)\n", id, refeicoes[id]);
        pthread_mutex_unlock(&garfo[dir]);
        pthread_mutex_unlock(&garfo[esq]);
        sem_post(&garcom);             // levanta da mesa
    }
    return NULL;
}

int main(void) {
    setbuf(stdout, NULL);
    sem_init(&garcom, 0, N - 1);       // no máximo 4 sentados de uma vez
    pthread_t t[N]; int id[N];
    for (int i = 0; i < N; i++) pthread_mutex_init(&garfo[i], NULL);
    for (int i = 0; i < N; i++) { id[i]=i; pthread_create(&t[i],NULL,filosofo,&id[i]); }
    for (int i = 0; i < N; i++) pthread_join(t[i],NULL);
    printf("todos comeram\n");
    return 0;
}

Código 10-4: O garçom limita a quatro filósofos à mesa, e o deadlock some.

$ cc main.c -o main -pthread
$ ./main
filosofo 0 comeu (1)
filosofo 0 comeu (2)
filosofo 0 comeu (3)
filosofo 1 comeu (1)
filosofo 1 comeu (2)
...
todos comeram

Cada filósofo come as três vezes e o programa termina. Nesta execução o filósofo 0 pegou os dois garfos e emendou as três refeições antes de soltar, porque o loop é rápido e ele estava com os garfos quentes na mão; noutra execução a ordem sai diferente. O que não muda é o essencial: o garçom nunca deixa os cinco disputarem ao mesmo tempo, então nunca se forma o anel de espera. Comparado com a ordem global, o garçom tem a vantagem de ser local: você não precisa reescrever a regra de aquisição de cada filósofo, só interpor uma permissão na entrada. É a mesma ideia do estacionamento com vagas, aplicada a uma disputa que sem o limite viraria deadlock.