Readers-Writers
Você já usou um read-write lock pronto: vários leitores juntos, um escritor sozinho. Agora vamos construir um, com as peças que já temos na mão. O exercício vale porque mostra que o rwlock não é mágica de sistema operacional: é um mutex, um semáforo e um contador arranjados de um jeito esperto, e ver o arranjo explica de onde vem a starvation que o capítulo do rwlock mencionou.
A ideia central é assimétrica. Um escritor precisa de exclusão total, então basta um
semáforo binário, escrita, que ele adquire antes de escrever e libera depois.
Leitores são o caso interessante: muitos podem entrar juntos, mas coletivamente eles
precisam manter os escritores de fora enquanto pelo menos um estiver lendo. A sacada
é fazer o primeiro leitor a chegar trancar o semáforo de escrita, e o último
a sair destrancar. Os leitores do meio nem tocam nesse semáforo, e é por isso que
entram todos juntos.
Para saber quem é o primeiro e quem é o último, os leitores mantêm um contador,
readcount. Incrementar e testar esse contador é uma seção crítica (dois leitores
não podem, ambos, achar que são o primeiro), então ele mesmo é protegido por um
mutex. O Código 10-2 monta tudo isso:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
int readcount = 0; // quantos leitores estão dentro
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; // protege o readcount
sem_t escrita; // 1 = livre pra escrever
int dado = 0;
void *leitor(void *arg) {
int id = *(int *)arg;
for (int i = 0; i < 2; i++) {
pthread_mutex_lock(&mtx);
readcount++;
if (readcount == 1) sem_wait(&escrita); // o primeiro leitor tranca a escrita
pthread_mutex_unlock(&mtx);
printf("leitor %d leu %d\n", id, dado); // vários podem estar aqui juntos
usleep(50000);
pthread_mutex_lock(&mtx);
readcount--;
if (readcount == 0) sem_post(&escrita); // o último leitor destranca
pthread_mutex_unlock(&mtx);
}
return NULL;
}
void *escritor(void *arg) {
int id = *(int *)arg;
for (int i = 0; i < 2; i++) {
sem_wait(&escrita); // exclusivo: espera nenhum leitor
dado++;
printf("escritor %d escreveu %d\n", id, dado);
usleep(50000);
sem_post(&escrita);
}
return NULL;
}
int main(void) {
sem_init(&escrita, 0, 1);
pthread_t l[3], w[2]; int lid[3]={0,1,2}, wid[2]={0,1};
for (int i=0;i<3;i++) pthread_create(&l[i],NULL,leitor,&lid[i]);
for (int i=0;i<2;i++) pthread_create(&w[i],NULL,escritor,&wid[i]);
for (int i=0;i<3;i++) pthread_join(l[i],NULL);
for (int i=0;i<2;i++) pthread_join(w[i],NULL);
printf("fim\n");
return 0;
}
Código 10-2: Um read-write lock montado com contador, mutex e um semáforo.
$ cc main.c -o main -pthread
$ ./main
leitor 0 leu 0
leitor 1 leu 0
leitor 2 leu 0
leitor 2 leu 0
leitor 0 leu 0
leitor 1 leu 0
escritor 0 escreveu 1
escritor 0 escreveu 2
escritor 1 escreveu 3
escritor 1 escreveu 4
fim
Olhe a ordem da saída. Os seis “leu” aconteceram primeiro, todos vendo dado = 0,
com vários leitores dentro ao mesmo tempo. Só depois que o último leitor saiu é que
os escritores conseguiram entrar. Não foi coincidência: enquanto chegava leitor, o
readcount nunca voltava a zero, o sem_post(&escrita) nunca era chamado, e os
escritores ficavam presos no sem_wait. Com leitores chegando sem parar, um escritor
poderia esperar pra sempre.
Essa é a starvation de escritor de que falamos ao ver a API do rwlock, agora visível na construção: a preferência por leitores não é uma decisão que alguém tomou por cima, é uma consequência direta de como as peças foram encaixadas. Esta é a solução original de Courtois, e ela favorece leitores por construção. Dar preferência a escritores, ou alternar de forma justa, exige mais contadores e mais semáforos, arranjados pra barrar novos leitores assim que um escritor manifesta interesse.1
-
A solução com preferência a escritores usa um segundo semáforo pra segurar leitores na entrada quando há escritor esperando, mais um contador de escritores análogo ao
readcount. É o mesmo tipo de raciocínio, só que aplicado aos dois lados. ↩