Criando Threads
Vamos estudar o modelo de concorrência mais fundamental de todos: Threads do SO comunicando-se entre si via memória compartilhada e sincronizando através de locks.
Nesse capítulo, vamos ver como criar threads nas principais linguagens de programação. Apesar de threads serem um construto do sistema operacional (portanto, vivem no kernel-space), as linguagens de programação precisam oferecer um tipo que represente essas threads no user-space.
Pthreads (POSIX Threads)
O padrão POSIX (Portable Operating System Interface) é uma coleção de interfaces de programação (APIs) que um sistema operacional deve implementar para ser considerado POSIX-compliant.
Esse padrão foi criado para que quando um programador escrevesse código que utilizasse as funcionalidades comuns de um sistema operacional (system calls, gerenciamento de processos e threads, utilitários de sistema, shells, etc), esse código funcionasse em qualquer sistema operacional POSIX-compliant.
Um dos componentes da POSIX é a API para criar e gerenciar threads do SO,
chamada de Pthreads. Essa API é acessível a partir da linguagem de
programação C, por meio do header <pthread.h>.
Criando uma única thread com pthread_t e pthread_create
Criar uma thread é simples:
#include <pthread.h>
#include <stdio.h>
void *greet(void *arg) {
char *name = (char *)arg;
printf("Hello from thread %s!\n", name);
return NULL;
}
int main() {
pthread_t my_thread;
pthread_create(&my_thread, NULL, greet, "worker");
}
Código 4-1: Criando uma thread com a Pthreads.
Vamos entender o código por partes:
-
Primeiro, para podermos usar as funções da Pthread, incluímos o header da biblioteca fazendo
#include <pthread.h>. -
Toda thread precisa executar algum código, e isso é feito por meio de uma função. Quando a thread inicia, ela executa uma função que o programador especifica. Nesse caso, a função
greetserá executada pela nossa thread.Nós chamaremos essas funções que são executadas pelas threads de “funções de thread”.
O primeiro e único argumento de
greetéarg, cujo tipo évoid *. Esse tipo é usado em C para representar um “ponteiro genérico”. Isto é, um ponteiro que pode estar apontando para um valor de qualquer tipo. Porém, não podemos usar um ponteiro desse tipo, apenas passar ele entre funções. O motivo pelo qual não podemos usar umvoid *(ponteiro paravoid) é porquevoidé um tipo que não possui valores. Portanto, não faz sentido tentar acessar/dereferenciar o valor apontado.Para podermos usar esse ponteiro, precisamos converter ele para um ponteiro para algum tipo válido. É o que fazemos em
char *name = (char *)arg;. Nós convertemos um ponteiro paravoidnum ponteiro parachar, e salvamos ele numa variávelname. Depois, imprimosnamena saída padrão e retornamosNULLpara indicar que essa função não retorna nada de útil.Toda função de thread tem a mesma assinatura: recebe um
void *e retorna umvoid *. -
Começamos o código da
maindeclarando uma variávelmy_threadcujo tipopthread_t. Esse tipo representa uma thread, mas por enquanto ela ainda não foi criada pelo sistema operacional.Para pedir que o SO crie a thread, chamamos
pthread_create. Precisamos passar:- um ponteiro para uma
pthread_t - um ponteiro para uma
pthread_attr_t, um tipo que armazena os atributos da nossa thread. Quando passamosNULL, a thread receberá os atributos padrões. - um ponteiro para a função de thread que será executada. Nesse caso,
passamos a função que criamos,
greet. Note que em C,&greeté equivalente agreet, pois o nome de uma função automaticamente decai para um ponteiro para ela mesma. - Um valor que será convertido num
void *e passado como o argumentoargdegreet. Note que passamos a string literal"worker", que é do tipochar[7]. Arrays em C decaem para um ponteiro do mesmo tipo, então o tipo decai dechar[7]parachar *, que é implicitamente convertido para umvoid *.
- um ponteiro para uma
Vamos executar o programa e vê-lo em ação:
$ cc main.c -o main -pthread -Wall
$ ./main
$
Epa! O que aconteceu?! O esperado era que o programa printasse Hello from thread worker! na saída padrão.
Aguardando uma thread concluir com pthread_join
O motivo de nada ter sido printado é que o nosso programa principal criou uma nova thread e chegou ao seu fim. O programa principal não ficou esperando até a thread ser de fato executada e encerrada.
Felizmente, podemos forçar que nosso programa principal (ou thread principal,
pois lembre-se que todo programa inicial com pelo menos uma thread, a principal)
fique aguardando a conclusão da sua irmã, usando a função pthread_join. É o
que fazemos no Código 4-2.
#include <pthread.h>
#include <stdio.h>
void *greet(void *arg) {
char *name = (char *)arg;
printf("Hello from thread %s!\n", name);
return NULL;
}
// --restante omitido--
int main() {
pthread_t my_thread;
pthread_create(&my_thread, NULL, greet, "worker");
pthread_join(my_thread, NULL);
}
Código 4-2: Usando pthread_join para aguardar a thread terminar de rodar.
Agora sim! pthread_join trabalha com dois parâmetros:
-
O primeiro é a variável que representa a thread que você deseja esperar.
-
O segundo é um
void **(um ponteiro para um ponteiro para void) que, após a thread sendo esperada finalizar, é atualizado para apontar para o valor retornado pela função de thread que acabou de ser executada (Lembre-se que o tipo do valor retornado por funções de thread é semprevoid *).Como passamos
NULL, estamos indicando que não queremos salvar esse valor. Depois veremos exemplos onde é útil para o programa obter o resultado retornado pela thread.
Criando múltiplas threads
Esse não seria um livro de concorrência se criássemos apenas uma thread! O
Código 4-3 cria 10 threads num loop, cada uma executando greet
com um argumento diferente.
#include <pthread.h>
#include <stdio.h>
void *greet(void *arg) {
char *name = (char *)arg;
printf("Hello from thread %s!\n", name);
return NULL;
}
// --restante omitido--
int main() {
int n = 10;
pthread_t threads[n];
char names[n][32];
for (int i = 0; i < n; i++) {
snprintf(names[i], sizeof(names[i]), "worker %d", i);
pthread_create(&threads[i], NULL, greet, names[i]);
}
for (int i = 0; i < n; i++) {
pthread_join(threads[i], NULL);
}
}
Código 4-3: Criando múltiplas threads.
#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];
printf("Thread sum: %ld\n", sum);
return (void *)sum;
}
int main(void) {
int numbers[N_ELEMS];
for (int i = 0; i < N_ELEMS; i++)
numbers[i] = i + 1; // 1..1000
size_t chunk = N_ELEMS / N_THREADS; // 200 each
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); // 500500
return 0;
}