Sistemas Operacionais: Threads

Durante nossa jornada pela Engenharia de Software, passamos rapidamente da escrita de scripts sequenciais para a necessidade de sistemas que realizam múltiplas tarefas simultaneamente. Para entender como um navegador web pode baixar uma imagem, responder aos cliques dos usuários e renderizar um vídeo ao mesmo tempo, precisamos falar sobre Threads.

O que é um(a) thread?

Em termos acadêmicos, uma thread (ou linha de execução) é a menor unidade de processamento que pode ser agendada por um sistema operacional. Ela reside dentro do contexto de um processo.

De acordo com Tanenbaum (2016, p. 53), enquanto um processo é uma maneira de agrupar recursos relacionados (como espaço de endereçamento, arquivos abertos e sinais), a thread é a entidade executada na CPU. Podemos imaginar que se o processo é o “contêiner” de recursos, a thread é o “fluxo” de execução de instruções.

Processos vs. Threads

Diferentemente dos processos, que são isolados uns dos outros por questões de segurança e integridade, as threads de um mesmo processo compartilham o mesmo espaço de memória (heap). No entanto, cada thread possui seu próprio:

  • Contador de Programa (PC): para saber qual instrução executar a seguir.
  • Registradores: para armazenar variáveis de trabalho atuais.
  • Pilha (stack): para gerenciar chamadas de funções e variáveis locais.

Na Figura 1 podemos ver um desenho esquemático que representa o conceito de ambiente monothread, ou seja, cada processo (círculo) contém apenas um fluxo – thread – para operação (retângulo dentro do círculo).

Figura 1 – Ambiente monothread.
Fonte: Machado e Maia (2014).

Em contrapartida, na Figura 2 encontramos o desenho esquemático do que seria um ambiente multithread. Um processo qualquer (círculo) contém três fluxosthreads – de operação (retângulos) em seu ambiente, compartilhando alguns recursos com esses threads.

Figura 2 – Ambiente multithread.
Fonte: Machado e Maia (2014).

Características fundamentais

As threads possuem propriedades que as tornam indispensáveis para o desenvolvimento de software moderno:

  1. Compartilhamento de recursos: elas compartilham o código, os dados e os recursos do sistema operacional (como arquivos abertos) do processo pai.
  2. Economia (lightweight): criar uma thread é muito mais “barato” em termos de processamento e memória do que criar um processo novo, pois não há necessidade de replicar todo o espaço de endereçamento.
  3. Concorrência e paralelismo: em sistemas com múltiplos núcleos (multi-core), threads diferentes podem ser executadas em núcleos diferentes simultaneamente (paralelismo real). Em núcleos únicos, o SO alterna entre elas tão rápido que cria a ilusão de simultaneidade (concorrência).

A Figura 3 traz uma representação mais ampla da separação de fluxos de trabalho dentro de um pseudocódigo com vários threads (Thread_1, Thread_2 e Thread_3) que realizam tarefas independentemente dentro do código geral.

Figura 3 – Desenho esquemático ampliado para um processo com vários threads.
Fonte: Machado e Maia (2014).

Benefícios e trade-offs

É importante sempre reforçamos que não existe “bala de prata” na computação. Toda escolha envolve trocas. Neste caso, a introdução de threads traz uma complexidade exponencial ao projeto. Portanto, falemos sobre os prós e os contras dessa técnica.

Benefícios

  • Responsividade: em aplicações de interface gráfica (GUI), uma thread pode processar a entrada do usuário enquanto outra realiza um cálculo pesado em segundo plano, evitando que a tela “trave”.
  • Utilização de arquiteturas multi-core: permite que a aplicação utilize todo o poder de processamento do hardware disponível.
  • Comunicação eficiente: como compartilham a mesma memória, a comunicação entre threads (inter-thread communication) é mais rápida que a comunicação entre processos.

Trade-offs

  1. Condições de corrida (Race conditions): quando duas threads tentam alterar o mesmo dado simultaneamente, o resultado final depende da ordem de execução, que é imprevisível. Isso exige o uso de mecanismos de sincronização como Mutexes e Semáforos.
  2. Deadlocks (Impasses): ocorre quando uma Thread A espera por um recurso reservado pela Thread B, enquanto a Thread B espera por um recurso reservado pela Thread A. Logo, ambas param para sempre.
  3. Dificuldade de debugging: erros em sistemas multithread são frequentemente não-determinísticos (difíceis de reproduzir), conhecidos como Heisenbugs.
  4. Overhead de troca de contexto: se houver threads demais para poucos núcleos, o sistema gasta mais tempo alternando entre elas do que executando o código propriamente dito.

Considerações finais

Dominar threads é o que separa um programador que apenas “faz funcionar” de um Engenheiro de Software que projeta sistemas robustos. Como afirma Silberschatz, Galvin e Gagne (2015), o projeto de aplicações multithreaded tornou-se uma necessidade absoluta com a estagnação das velocidades de clock e o foco da indústria em múltiplos núcleos de processamento.

Por isso, continue lendo os artigos deste site, além de consultar as fontes de referência utilizadas. Deixamos para os caros leitores os vídeos abaixo onde explicamos um pouco mais sobre threads e fizemos até um exemplo de código em linguagem C criando um ambiente multithread. Obrigado pela leitura e bons estudos.

Referências

MACHADO, Francis Berenger; MAIA, Luiz Paulo. Arquitetura de sistemas operacionais. 5ª ed. Rio de Janeiro: LTC, 2014.

SILBERSCHATZ, Abraham; GALVIN, Peter B.; GAGNE, Greg. Sistemas Operacionais com Java. 9. ed. Rio de Janeiro: LTC, 2015.

TANENBAUM, Andrew S. Sistemas Operacionais Modernos. 4. ed. São Paulo: Pearson Education do Brasil, 2016.