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

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:

  1. Primeiro, para podermos usar as funções da Pthread, incluímos o header da biblioteca fazendo #include <pthread.h>.

  2. 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 greet será 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 um void * (ponteiro para void) é porque void é 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 para void num ponteiro para char, e salvamos ele numa variável name. Depois, imprimos name na saída padrão e retornamos NULL para 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 um void *.

  3. Começamos o código da main declarando uma variável my_thread cujo tipo pthread_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:

    1. um ponteiro para uma pthread_t
    2. um ponteiro para uma pthread_attr_t, um tipo que armazena os atributos da nossa thread. Quando passamos NULL, a thread receberá os atributos padrões.
    3. 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 a greet, pois o nome de uma função automaticamente decai para um ponteiro para ela mesma.
    4. Um valor que será convertido num void * e passado como o argumento arg de greet. Note que passamos a string literal "worker", que é do tipo char[7]. Arrays em C decaem para um ponteiro do mesmo tipo, então o tipo decai de char[7] para char *, que é implicitamente convertido para um void *.

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:

  1. O primeiro é a variável que representa a thread que você deseja esperar.

  2. 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 é sempre void *).

    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;
}