Exercícios
Este capítulo te deu vocabulário preciso: data race (acesso concorrente à mesma memória, sem ordem, com ao menos uma escrita), race condition (corretude depende da ordem de escalonamento), a diferença entre os dois, o padrão check-then-act, a noção de seção crítica, e o ThreadSanitizer como detector de data races. Os exercícios giram em torno de reconhecer e classificar esses defeitos, que é a habilidade que o capítulo quis construir.
Compile o C com cc main.c -o main -pthread. Para o ThreadSanitizer, adicione
-fsanitize=thread -g.
Fáceis
7.1 Para cada situação, diga se há data race, race condition, os dois, ou nenhum:
- Duas threads leem a mesma variável global e nenhuma escreve.
- Dez threads fazem
counter++na mesma global, sem lock. - Uma thread escreve num arquivo e outra, sem coordenação, lê o mesmo arquivo.
- Cada thread escreve numa posição diferente de um array (
v[id] = ...), sem lock.
7.2 Rode o contador sob o ThreadSanitizer e leia a saída. Aponte, na mensagem do TSan, a linha do código-fonte que ele acusa, e diga qual thread fez a leitura e qual fez a escrita conflitante.
7.3 No exemplo do saque
(race condition sem data race), diga qual é a
seção crítica: o menor trecho de código que precisaria rodar de forma
indivisível pra sacar ficar correto.
Médios
7.4 Pegue a versão quebrada do contador e proteja o counter++ com um mutex
(você já sabe fazer isso do capítulo anterior). Rode de novo sob o
ThreadSanitizer. O TSan ainda acusa data race? Explique o que a proteção mudou
em relação às três condições que definem um data race.
7.5 O programa abaixo mantém um “máximo visto até agora” compartilhado.
Encontre a seção crítica e explique por que, mesmo cada novo > maximo sendo uma
leitura simples, o programa pode terminar com um maximo menor do que o maior
valor processado:
#include <stdio.h>
#include <pthread.h>
int maximo = 0;
int valores[][4] = {
{10, 55, 23, 80},
{70, 12, 91, 34},
{45, 88, 19, 60},
};
void *processa(void *arg) {
int *linha = arg;
for (int i = 0; i < 4; i++) {
int novo = linha[i];
if (novo > maximo) // checa
maximo = novo; // age
}
return NULL;
}
int main(void) {
pthread_t tids[3];
for (int i = 0; i < 3; i++)
pthread_create(&tids[i], NULL, processa, valores[i]);
for (int i = 0; i < 3; i++)
pthread_join(tids[i], NULL);
printf("maximo = %d (esperado: 91)\n", maximo);
}
Dica: é o mesmo formato check-then-act do saque, com
if (novo > maximo)no lugar doif (saldo >= valor).
7.6 Escreva você mesmo um exemplo, em Kotlin, de uma race condition sem
data race: cada acesso individual ao estado é @Synchronized, mas a lógica que
combina dois acessos ainda está errada. Não repita o saque do capítulo; invente
outra situação check-then-act (por exemplo, reservar o último ingresso de um
estoque, ou incrementar um id só se ele ainda não existe).
Difíceis
7.7 O capítulo afirma que data race e race condition são independentes, e dá os três casos. Construa, em código, um exemplo de cada:
- Data race e race condition ao mesmo tempo.
- Race condition sem data race.
- Data race sem race condition (um em que perder alguns acessos não muda nenhuma decisão do programa).
Para o caso 3, argumente por que ele continua sendo comportamento indefinido em C, apesar de “não estragar” o resultado que interessa.
7.8 No trecho de flag do capítulo do data race:
int done = 0;
void *worker(void *arg) {
while (!done) {
// faz trabalho
}
return NULL;
}
com outra thread fazendo done = 1 mais tarde, o worker pode nunca perceber a
mudança. Compile um programa com esse padrão em -O0 e depois em -O2 e veja se
o comportamento muda. Explique, com base no que o capítulo diz sobre
comportamento indefinido e otimização, por que o compilador tem permissão pra
transformar o laço em algo que nunca relê done.
Dica: você vai precisar de uma segunda thread que dorme um pouco (
usleep) e depois setadone = 1. Rode com timeout pra não travar o terminal caso o laço vire infinito.
7.9 Reforce o programa do exercício 7.5 até ele ficar correto usando um
mutex, e depois rode-o sob o ThreadSanitizer pra confirmar que o data race sumiu.
Em seguida, responda: proteger cada if (novo > maximo) com seu próprio par
lock/unlock, mas liberando o lock entre a checagem e a atribuição, resolve o
problema? Justifique ligando com o que o capítulo diz sobre check-then-act
precisar ser indivisível inteiro, não em metades.