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

E quando as threads precisam compartilhar dados?

Nos exemplos anteriores, fomos cuidadosos em garantir que cada thread trabalhasse sobre dados exclusivamente seus. Mas e se duas threads precisarem acessar e modificar a mesma variável?

O Código 5-5 cria 10 threads que incrementam um contador compartilhado, cada uma 100.000 vezes:

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

int counter = 0;

void *increment(void *arg) {
    for (int i = 0; i < 100000; i++)
        counter++;
    return NULL;
}

int main(void) {
    pthread_t tids[10];

    for (int i = 0; i < 10; i++)
        pthread_create(&tids[i], NULL, increment, NULL);
    for (int i = 0; i < 10; i++)
        pthread_join(tids[i], NULL);

    printf("counter = %d (esperado: 1000000)\n", counter);
}

Código 5-5: Contador compartilhado sem sincronização.

O resultado esperado é \( 10 \times 100000 = 1000000 \). Porém:

$ ./main
counter = 474032 (esperado: 1000000)
$ ./main
counter = 538691 (esperado: 1000000)
$ ./main
counter = 623107 (esperado: 1000000)

O valor muda a cada execução e é quase sempre menor que 1.000.000. Esse é o bug de lost update que analisamos no Capítulo 1: quando múltiplas threads leem, modificam e escrevem a mesma variável sem sincronização, incrementos são perdidos. Lá, vimos as intercalações possíveis de x = x + 1 executado por duas threads. Aqui, o mesmo problema aparece em escala. Com 10 threads e 100.000 incrementos cada, a quantidade de intercalações que causam perdas é astronômica.

“Mas é só somar no final”

Uma objeção justa a esse exemplo: o contador é artificial. Se cada thread mantivesse a própria soma numa variável local e a thread principal juntasse tudo no join, o problema sumia, e a gente cairia de volta no padrão dividir, processar, combinar do resto do capítulo. E é verdade. Todo agregado que só precisa do valor final (uma soma, um máximo, uma contagem) se resolve assim, sem thread nenhuma tocar o estado da outra.

O compartilhamento vira inevitável quando o estado precisa ser lido enquanto as threads ainda estão rodando, não depois. Aí não tem “somar no final”: o final é tarde demais.

Volte ao download concorrente. Suponha que, além de baixar, a gente queira uma barra de progresso: quantos arquivos já terminaram e quantos KB já foram baixados no total, atualizados de tempos em tempos enquanto os downloads acontecem. Duas variáveis compartilhadas, arquivos_prontos e kb_baixados, que cada thread de download atualiza ao avançar, e a thread principal lê num laço pra imprimir o placar:

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

typedef struct {
    const char *filename;
    int         size_kb;
} Download;

int arquivos_prontos = 0;
int kb_baixados       = 0;
int total_arquivos    = 0;

void *download_file(void *arg) {
    Download *d = arg;

    // "baixa" em pedaços de 100 KB, atualizando o progresso a cada pedaço
    int restante = d->size_kb;
    while (restante > 0) {
        int pedaco = restante < 100 ? restante : 100;
        usleep(pedaco * 100);
        kb_baixados += pedaco;
        restante -= pedaco;
    }

    arquivos_prontos++;
    return NULL;
}

int main(void) {
    Download files[] = {
        {"relatorio.pdf",  4200},
        {"foto.jpg",       1800},
        {"dados.csv",      3500},
        {"backup.tar.gz",  7200},
        {"config.json",      50},
    };
    total_arquivos = sizeof(files) / sizeof(files[0]);

    int total_kb = 0;
    for (int i = 0; i < total_arquivos; i++)
        total_kb += files[i].size_kb;

    pthread_t tids[total_arquivos];
    for (int i = 0; i < total_arquivos; i++)
        pthread_create(&tids[i], NULL, download_file, &files[i]);

    // enquanto os downloads rolam, a main lê o progresso e imprime
    while (arquivos_prontos < total_arquivos) {
        printf("progresso: %d/%d arquivos, %d/%d KB\n",
               arquivos_prontos, total_arquivos, kb_baixados, total_kb);
        usleep(200000);
    }

    for (int i = 0; i < total_arquivos; i++)
        pthread_join(tids[i], NULL);

    printf("progresso: %d/%d arquivos, %d/%d KB\n",
           arquivos_prontos, total_arquivos, kb_baixados, total_kb);
    printf("pronto.\n");
}

Código 5-6: Placar de progresso lido pela main enquanto as threads baixam.

Aqui não dá pra “juntar no final”. A barra de progresso só faz sentido durante a execução, e é justamente durante a execução que várias threads estão somando em kb_baixados ao mesmo tempo. É o mesmo x = x + 1 do contador, só que agora com um motivo palpável pra existir: a informação é útil no meio do caminho, não no fim. E o kb_baixados termina quase sempre abaixo dos 16.750 KB esperados, pela mesmíssima razão de antes.

O contador era o exemplo mínimo do problema; o placar é onde ele te pega num programa que você teria escrito de qualquer jeito. Nos dois casos falta a mesma coisa: um jeito de garantir que aquele incremento aconteça inteiro antes de outra thread mexer na variável.