Read-Write Lock
Imagine uma tabela de configuração que centenas de threads consultam o tempo todo e que muda de vez em quando. Proteger ela com um mutex funciona, mas paga um preço escondido. Duas threads que só querem ler a tabela não interferem uma na outra: ler não muda nada, e duas leituras simultâneas dão o mesmo resultado que duas leituras em sequência. Mesmo assim, o mutex serializa as duas, porque ele não faz distinção entre ler e escrever. Cada leitura espera a anterior terminar, e o paralelismo que existia de graça é jogado fora.
O read-write lock faz essa distinção. Ele tem dois modos de aquisição. Várias threads podem segurar o lock em modo de leitura ao mesmo tempo, sem se bloquearem. Mas o modo de escrita é exclusivo: enquanto uma thread escreve, nenhuma outra lê nem escreve. A regra é a do mundo real: quantos leitores você quiser, mas um escritor sozinho.
Medindo o ganho
O experimento tem oito threads batendo numa estrutura compartilhada, cada operação fazendo um trabalho de alguns microssegundos com o lock na mão. O que varia é a proporção de escritas. Comparamos o mesmo programa protegido por um mutex e por um rwlock:
== 0% escritas ==
mutex : 0.256s
rwlock: 0.030s
== 1% escritas ==
mutex : 0.257s
rwlock: 0.135s
== 10% escritas ==
mutex : 0.278s
rwlock: 0.576s
== 50% escritas ==
mutex : 0.282s
rwlock: 0.601s
Com leitura pura, o rwlock termina em um oitavo do tempo do mutex, quase o fator oito que você esperaria de oito threads que agora rodam em paralelo em vez de enfileiradas. Com 1% de escritas o ganho encolhe, mas ainda é quase o dobro. E aí a coisa vira: em 10% de escritas o rwlock já é mais lento que o mutex, e em 50% ele perde feio.
A virada tem duas causas. A óbvia é que toda escrita é exclusiva, então quanto mais
escritas, mais o rwlock se comporta como o mutex de qualquer jeito, sem paralelismo
nenhum para compensar. A menos óbvia é que o rwlock é mais caro por operação: para
deixar vários leitores entrarem juntos, ele precisa contar quantos estão dentro, e
esse contador é ele próprio um dado compartilhado que precisa ser atualizado de forma
atômica a cada rdlock e unlock. Quando as leituras são curtas e frequentes, essa
contabilidade extra come o ganho do paralelismo. O rwlock só vale a pena quando as
leituras dominam de verdade e seguram o lock por tempo suficiente pra que rodar
várias em paralelo compense o custo de contá-las.
A API da Pthreads
O tipo é pthread_rwlock_t, e a aquisição se desdobra em duas funções conforme a
intenção:
#include <stdio.h>
#include <pthread.h>
pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER;
int config = 42;
void *leitor(void *arg) {
pthread_rwlock_rdlock(&lock); // modo leitura: compartilhado
printf("leitor viu config = %d\n", config);
pthread_rwlock_unlock(&lock);
return NULL;
}
void *escritor(void *arg) {
pthread_rwlock_wrlock(&lock); // modo escrita: exclusivo
config = 99;
printf("escritor mudou config\n");
pthread_rwlock_unlock(&lock);
return NULL;
}
int main(void) {
pthread_t l[3], w;
for (int i = 0; i < 3; i++) pthread_create(&l[i], NULL, leitor, NULL);
pthread_create(&w, NULL, escritor, NULL);
for (int i = 0; i < 3; i++) pthread_join(l[i], NULL);
pthread_join(w, NULL);
return 0;
}
Código 9-3: Três leitores e um escritor sobre o mesmo rwlock.
O rdlock pega o lock em modo leitura e o wrlock em modo escrita; o mesmo
pthread_rwlock_unlock libera os dois. Um único inicializador estático,
PTHREAD_RWLOCK_INITIALIZER, serve pros dois modos, do mesmo jeito que o
PTHREAD_MUTEX_INITIALIZER servia pro mutex.
O escritor que nunca escreve
Deixar vários leitores entrarem juntos abre uma armadilha. Se leitores chegam sem parar e o lock sempre tem pelo menos um deles dentro, um escritor esperando o modo exclusivo pode nunca conseguir a vez. Sempre tem um leitor lá dentro, sempre chega outro antes do último sair, e o escritor fica na fila indefinidamente. É a starvation de novo, agora não por barging do mutex, mas embutida na regra de quem pode entrar: a preferência por leitores mata o escritor de fome.
As implementações lidam com isso escolhendo uma política. Algumas dão preferência a escritores: assim que um escritor chega e pede o lock, novos leitores param de ser admitidos e ficam esperando ele passar, mesmo que o lock ainda esteja em modo leitura. Isso protege o escritor, ao custo de segurar leitores que poderiam ter entrado. A Pthreads não fixa a política no padrão e deixa em aberto pra implementação, o que significa que um código que depende de uma preferência específica não é portável. Antes de escolher um rwlock, vale saber qual das duas starvations você prefere arriscar, porque não dá pra fugir das duas ao mesmo tempo.