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

Os exercícios abaixo usam só o que apareceu até aqui: criar threads com pthread_create, esperar com pthread_join, passar dados via struct e devolver resultado convertendo pra void *. Nada de mutex ainda, isso é o próximo capítulo.

Dividir, processar, combinar é um dos formatos que dá pra montar sem sincronização, e boa parte dos exercícios cai nele, mas não é o único. Alguns aqui não têm fase de combinação nenhuma (cada thread termina sozinha, como no download do Exemplo 3), e outros processam fatias sem reduzir a um valor só. O que amarra todos é a mesma condição: enquanto não existe lock, as threads só podem tocar regiões disjuntas da memória. Os exercícios difíceis esbarram de propósito nesse limite, pra você sentir onde ele está, não consertar (ainda).

Salvo indicação, compile com cc main.c -o main -pthread.

Fáceis

5.1 Modifique a soma paralela pra usar 8 threads em vez de 5, sobre um array de 10.000 elementos preenchido com i + 1. Confira que o total continua 50005000.

5.2 No exemplo da soma, o array tem 1000 elementos e são 5 threads, divisão exata. Troque N_ELEMS pra 1003 sem mexer em mais nada e rode. O resultado ainda fica certo? Explique, olhando pra linha que calcula slices[t].len, por que a última fatia é tratada diferente das outras.

5.3 Escreva um programa que cria 4 threads, cada uma recebendo via struct um número inteiro n (1, 2, 3, 4). Cada thread calcula n * n e devolve o resultado. A main coleta os quatro quadrados com pthread_join e imprime a soma deles (esperado: 1 + 4 + 9 + 16 = 30).

5.4 Pegue a busca do máximo e adapte pra encontrar o mínimo de um array de 16 inteiros. O que muda além do nome da função?

5.5 Escreva um programa que dobra cada elemento de um array de 1000 inteiros, em paralelo, escrevendo o resultado no próprio array (v[i] *= 2). Divida em fatias, uma por thread, e note que aqui não existe fase de combinação: cada thread escreve na sua fatia e acabou, o join só garante que tudo terminou antes da main imprimir. Compare com a soma paralela e diga o que sumiu.

Médios

5.6 Generalize a soma paralela pra funcionar com um número de threads que não divide o tamanho do array de forma exata (por exemplo, 1000 elementos e 3 threads). Garanta que nenhum elemento fique de fora nem seja somado duas vezes. Verifique com 1000 elementos e 3, 7 e 11 threads que o total é sempre 500500.

Dica: distribua os elementos que sobram (N_ELEMS % N_THREADS) entre as primeiras fatias, uma a mais em cada, em vez de jogar todos na última.

5.7 Escreva um programa que conta, em paralelo, quantos números primos existem num array de inteiros. Divida o array em fatias, cada thread conta os primos da sua fatia e devolve a contagem parcial, e a main soma as parciais. Teste com o array {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} (resposta: 6 primos).

5.8 Simule o envio de 6 notificações independentes, cada uma com um tempo de “envio” diferente (use usleep com valores como 300, 120, 250, 80, 400 e 150 ms, um por notificação, passados via struct). Rode primeiro sequencialmente, depois com uma thread por notificação, e meça o tempo de relógio das duas versões com clock_gettime, como no download concorrente. Não há nada pra combinar: cada thread dispara sua notificação e termina. Explique por que o tempo da versão concorrente fica perto da notificação mais lenta (400 ms), e não da soma de todas.

5.9 No download concorrente, a versão concorrente cria uma thread por arquivo. Suponha que você tenha 100 arquivos pra baixar, mas queira usar no máximo 5 threads. Descreva (em prosa ou pseudocódigo, não precisa compilar) como você organizaria o trabalho pra que cada thread processe uma fatia da lista de arquivos, uma depois da outra, respeitando o limite de 5 threads.

Difíceis

5.10 Rode o contador sem sincronização cinco vezes e anote os resultados. Agora reduza o número de incrementos por thread de 100.000 pra 10, mantendo as 10 threads, e rode mais cinco vezes. Com que frequência o resultado dá certo (100) em cada caso? Explique por que diminuir o trabalho dentro do laço muda a probabilidade de você observar o bug, mesmo o bug estando presente nos dois programas.

5.11 Considere este programa, que tenta acumular resultados num array compartilhado em vez de devolver pelo join:

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

#define N 4

int resultados[N];

typedef struct {
    int  id;
    int  valor;
} Tarefa;

void *worker(void *arg) {
    Tarefa *t = arg;
    resultados[t->id] = t->valor * 10;
    return NULL;
}

int main(void) {
    pthread_t tids[N];
    Tarefa tarefas[N];

    for (int i = 0; i < N; i++) {
        tarefas[i].id    = i;
        tarefas[i].valor = i + 1;
        pthread_create(&tids[i], NULL, worker, &tarefas[i]);
    }
    for (int i = 0; i < N; i++)
        pthread_join(tids[i], NULL);

    for (int i = 0; i < N; i++)
        printf("resultados[%d] = %d\n", i, resultados[i]);
}

Este programa não tem o bug do contador, mesmo com 4 threads escrevendo no mesmo array resultados sem sincronização nenhuma, e mesmo o array sendo estado compartilhado. Explique por quê. O que é diferente entre este caso e o counter++? Que propriedade das escritas de cada thread torna a ausência de sincronização segura aqui?

Dica: olhe em qual posição de memória cada thread escreve.

5.12 Agora quebre o programa de propósito. Modifique o worker do exercício anterior pra que todas as threads escrevam na mesma posição resultados[0] += t->valor (em vez de resultados[t->id] = ...). Rode várias vezes. O valor final de resultados[0] deveria ser 1 + 2 + 3 + 4 = 10. Ele é sempre 10? Relacione o que você observa com o bug do contador, e explique por que a mudança de resultados[t->id] pra resultados[0] reintroduziu o problema que o exercício 5.11 não tinha.