Exemplo 1: Soma paralela de um array
Vamos começar com um problema simples: somar todos os elementos de um array grande. A versão sequencial é trivial:
#include <stdio.h>
#define N 1000
int main(void) {
int numbers[N];
for (int i = 0; i < N; i++)
numbers[i] = i + 1;
long total = 0;
for (int i = 0; i < N; i++)
total += numbers[i];
printf("total = %ld\n", total);
}
Código 5-1: Soma sequencial de um array.
O resultado é \( \frac{1000 \cdot 1001}{2} = 500500 \).
Paralelizando com threads
A ideia é simples: dividimos o array em fatias (slices) disjuntas e damos cada
fatia pra uma thread diferente. Cada thread calcula a soma parcial da sua fatia.
Ao final, a thread principal coleta os resultados via pthread_join e soma
tudo.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define N_THREADS 5
#define N_ELEMS 1000
typedef struct {
const int *data;
size_t len;
} Slice;
void *partial_sum(void *arg) {
Slice *s = arg;
long sum = 0;
for (size_t i = 0; i < s->len; i++)
sum += s->data[i];
return (void *)sum;
}
int main(void) {
int numbers[N_ELEMS];
for (int i = 0; i < N_ELEMS; i++)
numbers[i] = i + 1;
size_t chunk = N_ELEMS / N_THREADS;
pthread_t tids[N_THREADS];
Slice slices[N_THREADS];
for (int t = 0; t < N_THREADS; t++) {
slices[t].data = numbers + t * chunk;
slices[t].len = (t == N_THREADS - 1)
? N_ELEMS - t * chunk
: chunk;
pthread_create(&tids[t], NULL, partial_sum, &slices[t]);
}
long total = 0;
for (int t = 0; t < N_THREADS; t++) {
void *res;
pthread_join(tids[t], &res);
total += (long)res;
}
printf("total = %ld\n", total);
}
Código 5-2: Soma paralela usando 5 threads.
O struct Slice armazena um ponteiro para o início de uma fatia do array e o
comprimento dessa fatia. Com isso, cada thread sabe exatamente quais elementos
deve somar, sem acessar dados de outras threads.
A função partial_sum recebe uma Slice, calcula a soma dos elementos e
retorna o resultado. O retorno é feito convertendo um long para void *,
truque que funciona pois em plataformas de 64 bits, void * e long
têm o mesmo tamanho (8 bytes)1.
No main, dividimos o array de 1000 elementos em 5 fatias de 200 cada. A
última fatia recebe os elementos restantes caso a divisão não seja exata.
Criamos 5 threads, cada uma recebendo sua Slice, e depois coletamos os
resultados com pthread_join.
O que faz esse exemplo funcionar sem nenhuma sincronização: as threads operam
sobre regiões disjuntas da memória. Cada thread lê apenas a sua fatia (que é
const) e escreve numa variável local sum. A única comunicação acontece no
join, quando a thread principal coleta os resultados.
-
Converter entre
longevoid *é uma prática comum em código baseado em Pthreads, mas tecnicamente é um comportamento definido pela implementação (não pelo padrão C). Em plataformas de 64 bits (Linux x86-64, ARM64), isso funciona pois ambos os tipos têm 8 bytes. Para código portável, a alternativa é alocar memória no heap e retornar um ponteiro, como fizemos no capítulo anterior. ↩