O pequeno artigo sobre threads

Adriano Croco
5 min readAug 28, 2020

--

Olá!

A regra número um de piadas referenciais é que se você explica a referência a piada perde a graça, mas, eu vou explicar mesmo assim: o título desse artigo é uma referência a um livro muito bom chamado a little book of semaphores, que você deveria dar uma olhada, caso se interesse em como os computadores lidam com problemas de concorrência.

Vamos começar entendendo alguns conceitos básicos necessários para evoluir nesse assunto como: o que são processos?

Um processo no sistema operacional é uma unidade de execução de programas, gerenciada pelo sistema operacional. A grosso modo, o sistema operacional controla a criação e destruição de processos, bem como a sincronização e ordenação do processamento, com mecanismos como schedulers. Um ponto que é importante mencionar é que o scheduler não somente cuida de “fazer as coisas na ordem correta”, mas também, “fornecer os recursos necessários” para se atingir determinado resultado.

Vou usar uma metáfora aqui para irmos para o próximo tópico: threads.

Pense em seu sistema operacional como uma empresa. O processo é um determinado departamento que tem tarefas a executar e precisa executá-las em uma determinada ordem para a empresa funcionar. Quem controla a ordem, sincronização e recursos para uma determinada tarefa ser executada? O Diretor de Operações (scheduler).

E o que as pessoas fazem dentro desse departamento? Elas trabalham. O que elas precisam para trabalhar? Recursos para executar uma determinada tarefa. E o que cada pessoa tem dentro de si? Memória e Cérebro para ajudá-la no que ela precisa fazer. As pessoas são threads nesse exemplo.

A grosso modo, a diferença de processamento single-thread para multi-thread é ilustrada pela seguinte imagem:

Usando os termos da imagem, cada thread tem seu próprio código (code) para execução, lida com seus próprios arquivos (files) e conjunto de dados (data). Assim como cada uma tem seu próprio registro de ações a executar (stack) e um registro (register) de instruções a ser passada para a CPU.

Então porque multi-thread é uma ideia boa? Porque eu to colocando mais pessoas no departamento para fazer um determinado trabalho. Assim como quanto maior a quantidade de pessoas, maior a quantidade de trabalho que eu consigo realizar, maior a necessidade de mecanismos de sincronização entre as partes para organizar tudo de uma forma melhor e sem conflitos.

Um outro detalhe aqui é que em alguns processadores é possível criar múltiplas threads dentro de cada core de um processador, não necessariamente precisa ser uma relação de 1:1 entre cores e threads.

Agora pensem que cada thread usa o modelo de memória compartilhada do artigo anterior e ocorre concorrência por recursos de memória dentro de um determinado processo.

Qual o nome dessa técnica computacional que permite gerenciar esse acesso a memória compartilhada? Primitivos de sincronização.

Geralmente quando falamos de primitivos, estamos falando de tipos de dados que não podem ser quebrados em partes menores, como inteiros, booleanos ou strings. Apesar da implementação low-level variar entre linguagens, de uma maneira geral você sabe o que esperar quando manipula um número ou texto na maioria das linguagens, não é mesmo?

O mesmo ocorre com os primitivos de sincronização. Cada sistema operacional implementa um conjunto deles, com pequenas variações entre eles. Vamos passar um pouco por alguns tipos e tentar entender melhor o que eles fazem:

Semaphores: é um inteiro que só pode ser incrementado (+1) ou decrementado (-1). Quando uma thread decrementa e o valor se torna < 0, ela bloqueia e aguarda até outra thread incremente o valor novamente. Quando isso ocorre, uma thread que estava aguardando é desbloqueada. Geralmente, é um mecanismo bem simples de usar e elegante, além de serem bastante eficientes, dado que só ocorre manipulação de inteiros.

Lock: é um dos primitivos mais comuns de se encontrar por aí, além de ser uma escolha intuitiva ao se tentar aplicar thread-safety no código. Basicamente consiste em uma thread “segurar um bastão” para acessar um recurso e só liberar após a utilização desse recurso compartilhado:

O que acontece caso ocorra um erro na thread que está com o lock? Deadlocks. Além disso, cabe um outro ponto de atenção: não é um dos mecanismos mais eficientes para se fazer isso: além de frágil, ele gera um overhead grande no “adquire trava/libera trava” que pode gerar problemas de performance caso usado em demasia. Quando se usa essa mecanismo para múltiplos processos recebe o nome de mutual exclusion ou mutex.

Signals: É simplesmente quando uma thread aguarda um sinal de outra para fazer algo. Essa ideia de passagem de mensagens é muito interessante e permite uma infinidade de patterns de processamento assíncrono, além de permitir a criação de sistemas de tempo real tolerante a falhas de verdade:

Pra finalizar: e se eu te disser que a chave para sistemas robustos é deixar tudo explodir do jeito certo? Não acredita? dá uma olhada aqui. Agora imagine usar toda essa estrutura robusta em uma linguagem moderna, brasileira e super promissora? Segue a dica:

Enter Elixir

Antes de explorar Elixir de fato, ainda pretendo passar por mais alguns tópicos dentro do tema “concorrência”, justamente para pavimentar o caminho para entendermos a beleza da linguagem.

Até!

Você gostou do conteúdo e gostaria de fazer mentoria comigo? Clique aqui e descubra como.

--

--