DEV Community

Cover image for Threads, processos, deadlock e race condition no contexto de concorrência e paralelismo
Artur Freire
Artur Freire

Posted on

Threads, processos, deadlock e race condition no contexto de concorrência e paralelismo

Navegando pelas redes sociais, você muito provavelmente já se deparou com aqueles quizzes “10 perguntas que todo formando em computação deveria saber responder”. Uma das perguntas que eu mais vejo aparecer nesses questionários é: qual é a diferença entre uma thread e um processo?

Você teria essa resposta na ponta da língua? Eu achava que sim até aprofundar minha leitura no assunto.

Figura 1 — Analogia comumente utilizada para responder à pergunta sobre threads e processos.

“Imaginando uma bebida, o processo seria o líquido e a thread seria o canudo”.

Esta é a analogia que mais fazem ao tentar responder à pergunta. No entanto, eu não acredito que ela relaciona os termos de forma adequada. Isto porque a hierarquia dos termos na analogia está invertida: não são as threads que “contêm os processos”, mas o processo é quem contém as threads.

Uma analogia mais apropriada seria a de que o processo é a fábrica, com seus maquinários e certificações de segurança, enquanto a thread seria o operador, a unidade de execução que desempenha suas funções em cima dos recursos que a fábrica reservou.

Imagine que estamos desenvolvendo um sistema para uma padaria de bairro com uma média de 50 vendas por dia. As vendas são registradas num ledger book de modo que, diariamente, é gerado um relatório com o saldo do caixa e o estoque atualizados. Por conta do baixo volume, a decisão de processar sequencialmente cada registro é ótima pois adequa a complexidade da solução ao tamanho da demanda, sendo possível obter o fechamento do dia em milissegundos.

Agora imagine que essa padaria cresceu e se tornou uma rede nacional com média de 1 milhão de vendas por dia. O processamento de um volume tão grande poderia levar horas se realizado por uma única thread. Neste caso, torna-se necessária a divisão do processamento em múltiplas threads, efetivamente implementando estratégias de concorrência e paralelismo que, embora muitas vezes sejam utilizadas como sinônimos, não são.

Além de otimizar o tempo ocioso nos ambientes single thread ao alternar entre tarefas (como se pode observar no Event Loop do Node.js), as estratégias de concorrência são fundamentais para a manutenção da consistência numa solução com paralelismo. Isto porque o ambiente multi-thread introduz complexidades inerentes à solução tais como o race condition e o deadlock.

Ao trabalhar com uma única thread, a consistência é garantida por definição pois o processamento é realizado de forma sequencial de modo que “uma única mão mexe nos dados”. No paralelismo, com os acessos simultâneos à memória compartilhada, surge a necessidade de responder às perguntas:

  1. O que fazer quando threads distintas concorrem pelo mesmo recurso ao mesmo tempo? (race condition)

  2. O que acontece quando uma thread precisa de um recurso que está bloqueado por outra?

A manutenção da consistência neste cenário representa um dos maiores desafios da computação moderna conforme argumenta o professor Edward A. Lee (UC Berkeley) em seu paper The Problems with Threads (2006):

[Parallelism] discards the most essential and appealing properties of sequential computation: understandability, predictability, and determinism.

Inclusive, uma solução inadequada para responder à segunda pergunta pode levar ao problema de deadlock onde duas ou mais threads ficam esperando indefinidamente por recursos que a outra possui:

Figura 2 — Diagrama para representação de um deadlock.

O deadlock só ocorre quando a estratégia de concorrência implementada apresenta as seguintes características:

  1. exclusão mútua (um acesso por vez por meio de locks, mutexes ou semáforos);

  2. hold-and-wait (a thread não libera o recurso reservado enquanto está esperando por outro);

  3. não preempção (um recurso reservado para uma thread não pode ser “tomado” por outra);

  4. espera circular (conforme abordado no parágrafo e imagem anteriores).

Apesar disso, no exemplo da padaria a probabilidade de acontecer um deadlock é praticamente inexistente. Isto porque trata-se de um problema do tipo Single Instruction, Multiple Data (SIMD) onde a mesma operação é aplicada sobre um conjunto de dados com Data Partitioning (cada thread cuida de uma faixa específica do dado). Quando não há compartilhamento de estado e o acesso aos dados acontece de maneira uniforme e independente por cada thread, o uso de locks tende a ser dispensável.

A computação paralela é um campo extenso que vem sendo estudado há décadas. Os tópicos vão desde a natureza do problema que está sendo resolvido (única instrução para múltiplos dados, múltiplas instruções para um único dado, etc.) até a escolha do modelo de distribuição e processamento das tarefas (task graph, master-slave, etc.), além de questões como o problema de Context Switching e os limites físicos do paralelismo.

Em suma, este artigo buscou mostrar como o hardware moderno é utilizado para a execução de tarefas simultâneas e a complexidade que isto traz. Mas e numa aplicação de banco de dados, por exemplo? Como os conceitos de paralelismo e concorrência são implementados? Isso é assunto para um próximo artigo.

Top comments (0)