Pesquisar conteúdos

Encontre uma trilha ou aula pelo assunto.

Experimente pesquisar por “governança”, “ITIL” ou “planejamento”.

Threads, concorrência e paralelismo

Entenda como threads compartilham recursos de um processo e diferencie concorrência, paralelismo e execução assíncrona.
Oficina compartilhada de um processo envia três linhas de thread que alternam em uma CPU e seguem simultaneamente por duas CPUs

Thread é uma continuidade dentro do processo

Uma thread é um fluxo de controle dentro de um processo: possui um ponto atual de execução, registradores, pilha e estado que permitem avançar instruções. O processo reúne memória, recursos e proteção; suas threads usam essa moldura para realizar trabalho.

Todo processo em execução possui ao menos uma thread em modelos como POSIX e Windows. Um processo multithread mantém mais de uma continuidade. Enquanto uma thread espera dados da rede, outra pode interpretar uma página e uma terceira pode responder a uma solicitação já pronta — desde que o programa tenha sido estruturado para isso.

Na aula anterior, acompanhamos programa, processo, estados e troca de contexto. Agora abriremos a estrutura do processo. A pergunta deixa de ser apenas “qual processo está ativo?” e passa a incluir “quais fluxos podem progredir, quais recursos compartilham e em que momento executam?”.

Threads compartilham recursos e preservam contexto

Threads do mesmo processo normalmente compartilham:

  • código e bibliotecas carregadas;
  • dados globais e heap;
  • espaço de endereçamento virtual;
  • descritores de arquivos, pipes e sockets;
  • diretório atual, ambiente e várias credenciais do processo;
  • limites e outros atributos definidos pela plataforma.

Cada thread precisa manter separadamente o que define sua continuidade:

  • identificador de thread;
  • registradores e contador de instrução;
  • pilha de chamadas e variáveis automáticas nela armazenadas;
  • estado de escalonamento e, conforme a plataforma, prioridade;
  • dados locais da thread, máscara de sinais ou estruturas equivalentes.
Um processo contém código, dados, heap e recursos compartilhados ao lado de três threads, cada qual com ID, registradores, contador e pilha próprios
O limite não é simplesmente ‘tudo compartilhado’. POSIX e Windows definem atributos do processo e atributos específicos da thread; runtimes podem acrescentar outras camadas.

Esse arranjo torna a comunicação eficiente: uma thread pode produzir um objeto no heap e outra pode consumi-lo sem copiar todo o processo. A mesma proximidade amplia o impacto de erros. Escrita fora dos limites, fechamento indevido de um descritor ou alteração sem coordenação pode afetar todas as threads e derrubar o processo inteiro.

Isolamento entre processos costuma ser mais forte porque cada um recebe espaço virtual e recursos próprios. Threads são mais acopladas. A escolha entre processos e threads não é apenas “pesado versus leve”: envolve isolamento de falhas, segurança, comunicação, implantação e suporte do runtime.

Thread do sistema não é qualquer tarefa do programa

Bibliotecas e linguagens oferecem abstrações como tarefas, futures, promises, corrotinas e goroutines. Elas expressam trabalho ou continuidade, mas não precisam corresponder uma a uma a threads do sistema operacional.

Um runtime pode multiplexar milhares de tarefas sobre um conjunto menor de threads, mover continuações entre threads ou executar várias etapas na mesma thread. Em outra plataforma, uma tarefa de CPU pode ser enviada a um pool. Portanto, “criei uma task” não prova “o sistema criou uma thread”.

Também existem threads gerenciadas diretamente por bibliotecas de sistema, como Pthreads, e threads nativas do Windows. Mesmo nesses casos, a API define atributos, criação, término e sincronização; o código não administra registradores físicos por conta própria.

Criar trabalho exige definir como ele termina

Em POSIX, pthread_create() inicia uma thread dentro do processo com uma função de entrada e atributos. A thread termina ao retornar dessa função, chamar pthread_exit() ou seguir outro mecanismo previsto. Outra thread pode usar pthread_join() para esperar seu término e recolher o valor associado.

Uma thread joinable conserva informações necessárias até que alguém faça o join. Uma thread detached libera esses recursos automaticamente ao terminar e não pode ser coletada da mesma forma. Isso é decisão de ciclo de vida, não indicação de que ela executa em segundo plano ou é menos importante.

No Windows, funções e runtimes oferecem contratos próprios para criar, aguardar e encerrar threads. Forçar término abrupto é perigoso: a thread pode manter locks, memória em estado intermediário ou recursos que não serão finalizados corretamente. Cancelamento robusto costuma ser cooperativo — o trabalho recebe um sinal, alcança um ponto seguro, desfaz ou conclui efeitos e então termina.

Criar uma thread para cada item também não é regra de desempenho. Cada uma consome pilha, estruturas do sistema e tempo de agendamento. Pools reutilizam um conjunto controlado de trabalhadores e aplicam fila, mas ainda precisam de limites, cancelamento e tratamento de sobrecarga.

Sequencial, concorrente e paralelo respondem a perguntas diferentes

Uma execução sequencial conclui uma etapa antes de iniciar a próxima no fluxo observado. É simples e pode ser a melhor escolha quando o trabalho é pequeno ou estritamente dependente.

Concorrência é a composição de trabalhos cujos períodos de vida se sobrepõem e que podem progredir independentemente. Em uma única CPU, o sistema pode intercalar pequenos intervalos: executa A, muda para B, atende C e retorna a A. Há progresso concorrente sem duas instruções dessas threads serem executadas no mesmo instante físico.

Paralelismo é a execução simultânea de mais de um trabalho, usando múltiplos processadores, núcleos ou unidades capazes de operar ao mesmo tempo. Um programa concorrente pode explorar paralelismo quando existem recursos e partes independentes. Ele também pode continuar concorrente em apenas uma CPU.

Três linhas do tempo com tarefas A, B e C: sequenciais, intercaladas em uma CPU e simultâneas em duas CPUs
Concorrência trata da estrutura e da sobreposição do progresso. Paralelismo descreve execução no mesmo intervalo físico. Uma aplicação pode possuir um, ambos ou nenhum.

Considere três operações da plataforma de estudos:

  • A busca o conteúdo no armazenamento;
  • B transforma uma imagem de capa;
  • C atualiza um índice de pesquisa.

Se B depende do conteúdo obtido por A, essa parte continua ordenada. C talvez possa progredir sobre outro item. Executar tudo sequencialmente desperdiça oportunidades; tentar paralelizar etapas dependentes produz erro ou espera. O desenho correto começa pelas dependências, não pela quantidade de threads.

Interleaving torna a ordem observada variável

Em execução concorrente, operações de diferentes threads podem se entrelaçar. A sequência global pode ser A1, B1, A2, C1, embora cada thread preserve sua própria ordem definida. Uma execução seguinte pode produzir outra combinação porque interrupções, eventos, carga e decisões do sistema mudam.

Isso não significa que qualquer ordem seja permitida. A linguagem, a biblioteca e o hardware definem regras de memória; o programa adiciona sincronização para preservar invariantes. Mas confiar na ordem que “sempre aconteceu nos testes” é frágil quando não existe contrato que a garanta.

Uma variável compartilhada ilustra o problema. Se duas threads leem o mesmo valor, calculam atualizações e gravam sem coordenação, uma escrita pode sobrescrever a outra. O resultado não é consertado apenas porque existem dois núcleos — o paralelismo pode tornar a sobreposição mais provável. A aula sobre condições de corrida será canônica para esses mecanismos.

O conteúdo de concorrência, isolamento e MVCC em bancos de dados enfrenta outra camada: transações simultâneas e estados persistentes. Um SGBD pode usar threads internamente, mas mutex de aplicação e isolamento transacional não são soluções intercambiáveis.

Assincronia descreve início, espera e continuação

Uma operação é assíncrona quando sua conclusão é separada da chamada inicial e o programa pode continuar fazendo algo útil, devolver controle ou registrar uma continuação em vez de bloquear até o resultado chegar.

Assincronia não garante paralelismo. Uma única thread pode dirigir um loop de eventos: inicia várias operações de rede, registra interesses e executa as continuações conforme eventos ficam prontos. Durante a espera externa, não existe necessidade de manter uma thread dedicada consumindo uma CPU.

Também é possível implementar uma interface assíncrona transferindo trabalho bloqueante para outra thread. Por isso, observar async, await, promise ou future não revela sozinho o mecanismo. A documentação da API e do runtime precisa dizer se a operação usa I/O assíncrona, pool, thread dedicada ou conclusão imediata.

Uma thread inicia entrada e saída, fica disponível para outro trabalho, recebe o evento de conclusão e coloca a continuação novamente na fila
A continuação não precisa retomar na mesma thread, salvo quando o runtime ou um contexto específico oferece essa garantia. Assíncrono descreve o contrato temporal, não a quantidade de CPUs.

No modelo async/await do C#, por exemplo, await pode ceder o controle até uma tarefa terminar sem bloquear a thread durante I/O. Trabalho intensivo de CPU usa outra estratégia quando precisa preservar responsividade ou obter paralelismo. Essa é uma implementação concreta, não uma regra de sintaxe para todas as linguagens.

CPU-bound e I/O-bound pedem decisões diferentes

Um trabalho CPU-bound passa a maior parte do tempo calculando. Redimensionar milhares de imagens, comprimir dados ou calcular hashes pode aproveitar múltiplos núcleos se os itens forem independentes e a sobrecarga não superar o ganho.

Um trabalho I/O-bound passa grande parte do tempo aguardando armazenamento, rede, banco ou outro serviço. Aumentar threads pode ocultar algumas esperas, mas uma interface assíncrona pode atender muitas operações pendentes com menos threads. O limite real talvez esteja no dispositivo, na conexão ou no serviço externo.

Classificar pela aparência é insuficiente. Uma requisição “de banco” pode executar consulta cara no servidor; uma tarefa “de arquivo” pode incluir compressão intensa. Meça tempo de CPU, espera, vazão, latência e uso do recurso limitante.

Mais threads não significam mais velocidade

Paralelizar possui limites:

  • a parte estritamente sequencial continua limitando o tempo total;
  • criar, trocar e coordenar threads adiciona trabalho;
  • disputa por locks transforma paralelismo em espera;
  • caches e largura de banda de memória são compartilhados;
  • muitas threads prontas aumentam filas e trocas de contexto;
  • tarefas pequenas podem custar menos quando executadas diretamente;
  • afinidade, NUMA e limites de contêiner alteram CPUs realmente disponíveis.

Uma máquina anunciar muitos processadores lógicos não garante que o processo possa usar todos. Políticas, cotas, outros programas e topologia importam. A próxima aula explicará como escalonadores escolhem trabalho pronto e quais métricas ajudam a avaliar a política.

O objetivo também pode não ser reduzir o tempo total. Concorrência pode melhorar responsividade: a interface continua atendendo eventos enquanto um cálculo ocorre. Assincronia pode melhorar escalabilidade: threads não ficam bloqueadas em cada espera. Paralelismo pode aumentar vazão. Declare a métrica antes de escolher o mecanismo.

Compartilhamento exige propriedade e coordenação

Antes de criar threads, liste quais dados são:

  1. imutáveis e compartilháveis com segurança;
  2. exclusivos de uma thread ou tarefa;
  3. transferidos de um produtor para um consumidor;
  4. compartilhados e mutáveis, exigindo protocolo explícito;
  5. externos, como arquivo ou banco, com coordenação própria.

Imutabilidade, particionamento e passagem de mensagens podem reduzir compartilhamento mutável. Quando ele é necessário, mutexes, semáforos, monitores, operações atômicas e filas oferecem contratos diferentes. Selecioná-los sem conhecer a invariável apenas desloca o erro.

Thread-safe também não significa “a operação composta é correta”. Uma coleção pode proteger cada método e ainda permitir que duas chamadas separadas observem um estado intermediário. Correção pertence à sequência e à regra do domínio, não somente ao tipo utilizado.

Observe threads como uma fotografia

No PowerShell, uma consulta somente leitura pode mostrar quantas threads cada processo expõe naquele instante:

Get-Process |
  Sort-Object ProcessName |
  Select-Object -First 5 Id, ProcessName,
    @{Name = "Threads"; Expression = { $_.Threads.Count }}

O número pode mudar antes mesmo de a saída terminar. Ele não informa quantas threads estavam executando simultaneamente, quanto trabalho cada uma realizou nem quantas tarefas do runtime existem. Para isso, são necessários rastreamento temporal, contadores e ferramentas específicas da plataforma.

Não altere prioridade ou afinidade para “otimizar” a partir de uma listagem. Primeiro estabeleça hipótese, período representativo e métrica. Uma fotografia de muitas threads pode indicar pool ocioso, biblioteca interna, workers ativos ou vazamento; o contexto decide.

Erros comuns

  • “Thread é um processo menor.” Ela pertence a um processo e compartilha sua moldura de recursos.
  • “Toda variável é compartilhada.” Pilha e dados locais da thread são distintos; heap e globais costumam ser compartilhados.
  • “Task, coroutine e thread são sinônimos.” Runtimes podem multiplexar tarefas sobre threads de várias formas.
  • “Concorrente significa simultâneo.” Interleaving em uma CPU já permite concorrência sem paralelismo.
  • “Paralelo significa assíncrono.” Um cálculo paralelo pode bloquear o chamador até todas as partes terminarem.
  • async cria uma thread.” A operação pode usar I/O não bloqueante e continuar depois sem thread dedicada durante a espera.
  • “Mais threads sempre aceleram.” Coordenação, contenção e recursos limitantes podem reduzir desempenho.
  • “Thread-safe torna qualquer sequência correta.” Invariantes que abrangem várias operações continuam exigindo coordenação.
  • “Uma thread esperando está usando a CPU.” Esperas bloqueantes normalmente retiram a thread da fila executável até o evento ocorrer.
  • “Se funcionou uma vez, a ordem está garantida.” Sem contrato, o interleaving pode mudar entre execuções.

O que você deve guardar

Processo é a moldura de memória, recursos e proteção; thread é uma continuidade executável dentro dela. Threads compartilham código, heap e recursos, mas preservam identificador, registradores, pilha e outros atributos necessários para continuar corretamente.

Concorrência organiza trabalhos sobrepostos e pode existir por interleaving em uma CPU. Paralelismo exige execução simultânea. Assincronia separa início e conclusão, permitindo ceder controle durante uma espera; não implica automaticamente nova thread ou múltiplos núcleos.

Cargas de CPU e de I/O pedem estratégias diferentes. Mais threads podem ajudar, não mudar nada ou piorar o resultado. Dependências, isolamento, propriedade dos dados, limites e medição devem orientar o desenho.

Na próxima aula, entenda como o sistema escolhe entre trabalhos prontos e compare filas, prioridades e algoritmos em escalonamento de CPU.

Referências