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

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:

  1. Duas threads leem a mesma variável global e nenhuma escreve.
  2. Dez threads fazem counter++ na mesma global, sem lock.
  3. Uma thread escreve num arquivo e outra, sem coordenação, lê o mesmo arquivo.
  4. 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 do if (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:

  1. Data race e race condition ao mesmo tempo.
  2. Race condition sem data race.
  3. 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 seta done = 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.