Threads POSIX

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.

 

Estados de operação de uma thread.
Figura 1: Estados de operação de uma thread.

 

 

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.

 

 

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.

 

 

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.

 

 

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.

 

 

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.

 

 

 

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.

 

 

Para compilar esse arquivo, a seguinte linha de comando foi utilizada:

 

Já a execução do programa:

 

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.

 

 

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. 

 

 

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

 

Imagem destacada.

Licença Creative Commons
Threads POSIX por Fernando Deluno Garcia. Esta obra está licenciado com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.