Linux API: pthreads – Parte 1

Processadores de múltiplos núcleos já são uma realidade nos computadores da grande maioria dos usuários comuns. Em meio a esse avanço podemos escrever programas que executam várias linhas de processamento em paralelo, podendo fazer com que esses programas tenham um tempo de resposta menor.

Para tal, este post mostrará como utilizar POSIX threads na linguagem C.

O que são threads?

Threads são linhas de execução para processos. Ao iniciar um processo, uma thread principal é iniciada, um processo então pode criar outras threads para que estas executem tarefas específicas e de forma paralela. Quando temos uma CPU com um único núcleo, dizemos que as threads executam de forma concorrente, uma vez que a CPU só pode executar uma thread por vez, mas há mais que uma para ser executada ao “mesmo tempo”. Já quando temos uma CPU com vários núcleos, as threads podem ser executadas tanto de forma concorrente como em paralelo, pois o scheduler pode priorizar outros programas do computador além do seu.

Para que são utilizadas?

Como dito anteriormente, as threads são utilizadas para fazerem tarefas de forma simultânea dentro do mesmo processo. Para exemplificar, imagine um servidor de chat. Você poderia fazer tudo rodar na mesma thread: identificar novos usuários conectados, receber mensagens destes usuários e enviar estas mensagens para os usuários online, identificar usuários que deixam o sistema, e etc. Nesse exemplo, podemos ver que este processo se torna custoso para uma única thread (thread principal nesse caso). Para tornar este processo mais otimizado, podemos criar uma thread para tratar as conexões ativas (usuários conectando e desconectando do servidor), e outra thread somente para receber as mensagens.

Porque o nome pthreads?

POSIX threads, aka pthreads, é um padrão definido para sistemas *NIX, tais qual BSD e Linux. Outros sistemas operacionais, como o Windows, tem sua própria API de threads.

Exemplo

Segue abaixo um exemplo simples da utilização de pthreads em código C. Podemos ver duas threads sendo criadas, fazendo somas, e no final a thread principal pegando o retorno destas threads e somando os dois resultados:

Para compilar o código acima é necessário fazer o link com a libpthread:
gcc pthread1.c -o pthread1 -lpthread

Vamos ao código:
pthread_t é o identificador da thread, ou seja, é necessário utilizar este identificador para executar outras ações na thread, como o join(ver abaixo). Neste exemplo, criamos um array de pthreads com duas posições, pois teremos duas threads. Poderíamos ter criado duas variáveis ao invés do array, mas isto não muda o resultado final.

pthread_create é a função onde é criada uma nova thread de fato. Esta função recebe:
* pthread_id, que é o identificador da thread a ser criada
* pthread_attr_t, que é uma variável onde podemos setar alguns atributos para alterar o comportamento padrão da thread. Na maioria das vezes esse valor pode ser omitido, como no exemplo. Este assunto será abordado na parte dois do artigo.
* void *(*start_routine) (void *), que é um ponteiro de função que será executada pela thread. A definição do parâmetro no man page parece ser meio confusa, mas é bem simples: É esperado um nome de função, mas esta função deve retornar um ponteiro void, e deve receber um ponteiro void como parâmetro. No nosso exemplo, criamos a função func que atende os requisitos acima. Como já descrito, a função só pode receber um único parâmetro, e se você deseja passar mais de um valor, basta criar uma struct e utilizar este como parâmetro.
* void *arg, que é o parâmetro que será passado a função. Como este parâmetro é um ponteiro void, ele pode ser qualquer coisa. Basta passar o valor e fazer um cast para void * e o valor será passado para a função quando ela iniciar.

Tudo isso parece um pouco complicado de início, mas vamos a um resumo disso tudo acima:
pthread_create vai criar uma nova thread, que irá executar uma função que retorna um ponteiro void (pode retornar qualquer tipo de valor, sendo int, ou até mesmo uma struct), e que recebe como parâmetro um ponteiro void (que pode ser um int ou até uma struct). Acredito que tenha ficado mais claro agora!

A chamada pthread_create retorna zero em caso de sucesso. Qualquer valor diferente de zero significa que houve um erro.

No exemplo criamos duas threads, passando valores diferentes para cada uma delas, e assim esperamos que cada uma delas retorne um valor diferente. As duas threads irão executar a função func, que soma 100 vezes o valor passado como parâmetro. Podemos ver aqui como um int sofre um cast para void * na chamada pthread_create, e novamente sofre outro cast dentro da função func para int novamente. Desta forma a função pthread_create pode receber qualquer tipo de dado como parâmetro.

Após a criação das duas threads, a thread principal continua seu fluxo. A fim de “esperar” que as duas threads iniciadas terminem, a chamada pthread_join é necessária.

pthread_join é responsável por “esperar” por uma thread acabar, ou se ela já acabou, a chamada retorna imediatamente. Os parâmetros são:
* pthread_t, que é o identificador da thread
* void **retval, que pede um endereço para um ponteiro void. Esse parâmetro recebe o retorno da thread em questão, nesse caso o valor do cálculo.

Da mesma forma que pthread_create, pthread_join também retorna zero em caso de sucesso, e outro valor em caso de erro.

Ao executar o programa do exemplo, a saída a seguir pode ser vista:

[[email protected] pthreads]$ ./pthread1
Resultado: 1100

Espero que tenham gostado desta primeira parte! Não esqueça de comentar, até mais!

Referências:
Wikipédia (BR) – Threads
The Gekk Stuff – Linux Threads Introduction
man pthreads
pthread_create
man pthread_join