Olá, caro leitor. Este artigo tem como objetivo apresentar as funções básicas da biblioteca pthread. Assim, uma breve introdução sobre processos e threads é apresentada antes de apontar possíveis aplicações.
Sobre Processos e Threads
Um dos conceitos mais importantes relacionados a sistemas operacionais é denominado processo. De modo geral, um processo é uma abstração de um programa em execução. Tal programa possui um espaço de endereçamento e, em sistemas tradicionais, apenas um thread (segmento ou fluxo de controle) de execução. Além disso, o processo contém toda informação relacionada ao seu contexto de execução, por exemplo, o contador de programa, apontador de pilha e demais registradores. Dito de outra maneira, é um programa com com sua função principal, denominada main, sendo executado sequencialmente, instrução por instrução.
No entanto, em alguns sistemas é possível criar mais de um thread no mesmo processo, isto é, no mesmo espaço endereçamento. Nesse caso, mais de um fluxo de execução ocorre dentro do mesmo processo! Diante disso, é importante destacar algumas aplicações:
- Tratar atividades que ocorrem “simultaneamente”;
- Dividir a aplicação em tarefas que acessam recursos compartilhados;
- Reduzir o tamanho de uma aplicação, uma vez que threads ocupam menos espaço em relação aos processos;
- São mais fáceis de criar e destruir;
- A sobreposição de tarefas pode acelerar a aplicação;
- Possibilitam paralelismo real em sistemas multicore.
Principais diferenças
É importante destacar que threads e processos são conceitos diferentes. Como dito anteriormente, o processo é basicamente um agrupador de recursos (código e dados) e possui uma identidade, enquanto as threads são criadas no contexto de um processo e compartilham o mesmo espaço de endereçamento. À vista disso, threads não são independentes como os processos. Pois, embora compartilhem o mesmo espaço de endereçamento dentro de um processo, cada thread possui os mecanismos para gerenciar seu contexto de execução. Assim, threads possuem seu próprio contador de programa, apontador de pilha e registradores.
Contexto de threads
Os threads criados ocupam a CPU do mesmo modo que o processo criador, e também são escalonadas pelo próprio processo. Nesse contexto, quando uma aplicação multithread é executada, esses threads podem estar em qualquer um destes estados: em execução, bloqueado (aguardando), pronto para ser executado ou concluído (finalizado). Isso é ilustrado na Figura 1.

Portabilidade de Aplicações
Para padronizar a utilização de threads em diversos sistemas o IEEE estabeleceu o padrão POSIX threads (IEEE_1003.1c), ou Pthreads. Esse padrão define mais de 60 funções para criar e gerenciar threads. Tais funções são definidos na biblioteca pthreads.h. Além disso, a biblioteca define estruturas de dados e atributos para configurar os threads. De modo geral, esses atributos são passados como argumentos para os parâmetros das funções, por exemplo:
- pthread_t: Handle para pthread, isto é, um valor que permite identificar o thread;
- pthread_attr_t: Atributos para configuração de thread.
Esses recursos são utilizados nas principais funções para criação e gerenciamento de threads. Tais funções são apresentadas a seguir. Cabe ressaltar que as funções que retornam valor, tem como padrão o inteiro 0, indicando sucesso. Assim, qualquer inteiro diferente de zero é um código de erro.
Criando e destruindo threads
A função pthread_create é utilizada para inicializar um thread. Para isso, a função recebe como argumento o endereço de um dado pthread_t que será inicializado. Além disso, o endereço da função que será executada é informado no parâmetro start_routine. Tal função tem como retorno um valor void * e recebe apenas um argumento a partir do seu parâmetro void *. Esse argumento é passado para função start_routine especificando o último parâmetro da função pthread_create, denominado arg. Cabe ressaltar que essa função pode receber um atributo para configuração do thread. No entanto, esse argumento pode ser NULL, indicando que o thread será configurado conforme o padrão.
int pthread_create (pthread_t *thread, pthread_attr_t *attr, void *(*start_ routine) (void *), void *arg);
Um thread pode ser finalizado a partir da função pthread_exit. Essa função recebe como argumento um endereço que é utilizado para armazenar o valor de retorno do thread.
void pthread_exit (void *retval);
Gerenciando threads
Considerando um ambiente em que mais de um thread estão sendo executados, pode ser necessário, em algum momento, aguardar a finalização de um procedimento. Isso pode ser realizado com a função pthread_join. A função pthread_join recebe como argumentos a estrutura de controle do thread, do tipo pthread_t, que será finalizado e o endereço de um ponteiro (void **) para o valor de retorno do thread.
int pthread_join (pthread_t thread, void **thread_return);
No contexto de execução do thread é possível obter o identificador do thread, denominado thread ID. Isso é realizado com a chamada pthread_self que tem como retorno um valor do tipo pthread_t.
pthread_t pthread_self (void);
Como dito anteriormente, um thread pode estar em diversos estados. Em sua execução o thread pode indicar para gerenciador que o mesmo pode ser bloqueado. Isso é realizado pela função sched_yield. Nesse caso, um outro thread entrará em execução.
int sched_yield (void);
Exemplos
Para exemplificar alguns conceitos apresentados e destacar a utilização da biblioteca pthreads, alguns programas em C foram desenvolvidos e compilados usando o GCC no ambiente Linux.
O primeiro exemplo demonstra a criação de threads e suas funções de gerenciamento, o objetivo é apenas mostrar a utilização das funções.
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
/*Rotina que será executada*/
void * routine(void *arg);
int main (int argc, char *argv[])
{
pthread_t thread_id;
void * thread_res;
int rstatus;
/*tenta iniciar o thread, indicando a função 'routine' e passando como argumento a string "Minha primeira Thread!"*/
rstatus = pthread_create (&thread_id, NULL, routine, (void*)("Minha primeira Thread!"));
/*verificar se ocorreu algum erro na chamada de pthread_create*/
if(rstatus != 0)
{
printf ("Erro ao criar o thread.\n");
exit(EXIT_FAILURE);
}
printf ("Thread criado com sucesso!\n");
/*aguarda finalização do thread identificado por thread_id. O retorno é passado pelo ponteiro thread_res*/
rstatus = pthread_join (thread_id, &thread_res);
/*verificar se ocorreu algum erro na chamada de pthread_join*/
if(rstatus != 0)
{
printf ("Erro ao aguardar finalização do thread.\n");
exit(EXIT_FAILURE);
}
/*exibe o valor de retorno da função 'routine'*/
printf ("Thread finalizado! Retorno = %s\n", (char *)thread_res);
return 0;
}
void * routine(void *arg)
{
/*exibe o argumento recebido*/
printf("Argumento: %s\n", (char *)arg);
/*finaliza a função retornando o argumento que foi recebido*/
pthread_exit(arg);
}
Para compilar esse arquivo, a seguinte linha de comando foi utilizada:
$ gcc main.c -o main -lpthread
Já a execução do programa:
$ ./main
O resultado da execução do programa é mostrado abaixo. Como esperado, o thread é criado e a aplicação principal aguarda sua finalização.
| Thread criado com sucesso! Argumento: Minha primeira Thread! Thread finalizado! Retorno = Minha primeira Thread! |
No segundo exemplo, a mesma rotina é utilizada. No entanto, dois threads são criados.
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
/*Rotina que será executada pelos dois threads*/
void * routine(void *arg);
int main (int argc, char *argv[])
{
pthread_t thread_idA;
pthread_t thread_idB;
void * thread_res;
int rstatus;
/*tenta iniciar o thread, indicando a função 'routine' e passando como argumento a string "Thread A"*/
rstatus = pthread_create (&thread_idA, NULL, routine, (void*)("Thread A"));
/*verificar se ocorreu algum erro na chamada de pthread_create*/
if(rstatus != 0)
{
printf ("Erro ao criar o thread A.\n");
exit(EXIT_FAILURE);
}
printf ("Thread A criado com sucesso!\n");
/*tenta iniciar o thread, indicando novamente a função 'routine' e passando como argumento a string "Thread B"*/
rstatus = pthread_create (&thread_idB, NULL, routine, (void*)("Thread B"));
/*verificar se ocorreu algum erro na chamada de pthread_create*/
if(rstatus != 0)
{
printf ("Erro ao criar o thread B.\n");
exit(EXIT_FAILURE);
}
printf ("Thread B criado com sucesso!\n");
/*aguarda finalização do thread A identificado por thread_idA. O retorno é passado pelo ponteiro thread_res*/
rstatus = pthread_join (thread_idA, &thread_res);
if(rstatus != 0)
{
printf ("Erro ao aguardar finalização do thread A.\n");
exit(EXIT_FAILURE);
}
printf ("Thread A finalizado! Retorno = %s\n", (char *)thread_res);
/*aguarda finalização do thread B identificado por thread_idB. O retorno é passado pelo ponteiro thread_res*/
rstatus = pthread_join (thread_idB, &thread_res);
if(rstatus != 0)
{
printf ("Erro ao aguardar finalização do thread B.\n");
exit(EXIT_FAILURE);
}
printf ("Thread B finalizado! Retorno = %s\n", (char *)thread_res);
return 0;
}
void * routine(void *arg)
{
int contador = 10;
/*procedimento para decrementar um contador e exibir o seu valor*/
while(contador--)
{
printf("%s: %i\n", (char *)arg, contador);
}
/*finaliza a função retornando o argumento que foi recebido*/
pthread_exit(arg);
}
O resultado da execução do programa é mostrado abaixo. Nesse programa, devido ao procedimento simples executado pela rotina, o thread foi finalizado antes que pudesse ser trocado por outro.
| Thread A criado com sucesso! Thread B criado com sucesso! Thread B: 9 Thread B: 8 Thread B: 7 Thread B: 6 Thread B: 5 Thread B: 4 Thread B: 3 Thread B: 2 Thread B: 1 Thread B: 0 Thread A: 9 Thread A: 8 Thread A: 7 Thread A: 6 Thread A: 5 Thread A: 4 Thread A: 3 Thread A: 2 Thread A: 1 Thread A: 0 Thread A finalizado! Retorno = Thread A Thread B finalizado! Retorno = Thread B |
Isso pode ser modificado alterando a rotina de execução, adicionando a chamada da função sched_yield. Como dito anteriormente, essa função causa a interrupção do thread.
void * routine(void *arg)
{
int contador = 10;
/*procedimento para decrementar um contador e exibir o seu valor*/
while(contador--)
{
printf("%s: %i\n", (char *)arg, contador);
sched_yield();
}
/*finaliza a função retornando o argumento que foi recebido*/
pthread_exit(arg);
}
O resultado da execução do programa é mostrado abaixo. Em comparação com o segundo caso, é possível observar que agora os threads tem sua execução alternada.
| Thread A criado com sucesso! Thread B criado com sucesso! Thread B: 9 Thread A: 9 Thread B: 8 Thread A: 8 Thread B: 7 Thread A: 7 Thread B: 6 Thread A: 6 Thread B: 5 Thread A: 5 Thread B: 4 Thread A: 4 Thread B: 3 Thread A: 3 Thread B: 2 Thread A: 2 Thread B: 1 Thread A: 1 Thread B: 0 Thread A: 0 Thread A finalizado! Retorno = Thread A Thread B finalizado! Retorno = Thread B |
Conclusões parciais
Esse artigo teve como objeto fundamentar o conceito de threads e apresentar o padrão conhecido como POSIX Threads, também chamado de pthreads. No entanto, a aplicação desse recurso envolve outros aspectos que não foram abordados neste artigo. Pois, a partir do momento em que a aplicação é dividida em segmentos que são executados de forma alternada, e tais segmentos acessam a mesma informação, vem à tona questões voltadas ao compartilhamento de recursos e sincronização de operações. Essas características, se não forem tratadas corretamente, podem gerar uma grande confusão e, consequentemente, resultar em erro de execução de um programa.
Referências





muito bom