Concorrência vs. Paralelismo

Adriano Croco
5 min readAug 26, 2020

--

Olá!

Um assunto que sempre me fascinou durante minha carreira foi — além da escrita do software em si — como escrever programas que executam instruções de forma concorrente e/ou em paralelo. Geralmente esse tipo de abordagem oferece um grau maior de desafio. Logo, usando a máxima clássica dos investimentos: quanto maior risco, maior o retorno, certo?

Jovens, não invistam em algo que vocês não conhecem porque vocês vão perder tudo.

Continuando.

Primeiramente, vamos tentar nomear as coisas para facilitar. Citando Rob Pike (um dos criadores da linguagem Go):

Concorrência é sobre lidar com várias coisas ao mesmo tempo. Paralelismo é fazer várias coisas ao mesmo tempo.

Ficou abstrato?

Vamos tentar resumir em uma imagem:

Acredito que o principal motivo de usarmos os termos de forma intercambiável é porque a concorrência gera a sensação de paralelismo, dado que você tem um único core respondendo a chamadas simultâneas usando algum tipo de técnica de engenharia, como a pattern Reactor (que é a utilizada no NodeJS):

Nesse exemplo do NodeJs, os handlers estão na mesma thread. Por isso que temos problemas de performance em aplicações desse tipo caso você execute operações pesadas de I/O (Como chamada de rede ou operações no disco) dentro do event loop.

Como concorrência é sobre engenharia e paralelismo é sobre hardware, vamos tentar entender um pouco do que torna o paralelismo possível em termos de hardware.

Temos os seguintes tipos de técnicas:

Bit-Level: a quantidade de bits de uma CPU indica a forma o endereçamento de memória ocorre, portanto, quanto maior os bits, maior o endereçamento disponível (e mais memória se torna utilizável por consequência). Uma CPU de 64 bits que precise somar números grandes — digamos, de 64 bits, por exemplo — terá um desempenho melhor dado que não precisará efetuar duas operações para lidar com esse número, ao contrário de uma CPU de 32 bits que teria que fazer duas operações (pois, 32+32 = 64), ou 4 operações como uma de 16 bits faria (pois, 16*4 = 64). Por isso quanto mais bits, mais eficiente o processamento se torna, logo, mais rápido.

Instruction-Level: Algumas CPUs modernas possuem otimizações que permitem “prever o futuro” e executam instruções antes delas serem realmente necessárias, gerando ganhos de performance incríveis se usadas corretamente. Obviamente, algumas instruções podem ser processadas erroneamente, porque ninguém consegue calcular o futuro… pelo menos, não ainda. Nesse caso, há um rollback e a CPU reprocessa as instruções antecipadas erroneamente. Mas lembrem-se: isso afeta a performance caso ocorra.

Data Parallelism: Ou SIMD, é uma técnica que vetoriza um determinado conjunto de dados e executa a mesma instrução nesses dados. Um uso moderno disso seria processamento de imagens (afinal, pixels podem ser vetorizados). Aqui a gente tá falando de GPUs se saindo melhor nesse tipo de processamento. E o que acontece quando os jovens (ou empresas chinesas) descobrem esse tipo de técnica? simples, mineração de bitcoins. Pelo menos agora você sabe como aquele vírus que minera bitcoin funciona no low-level :)

Task-Level: aqui estamos falando de uma abstração de paralelismo orientada a tarefas. Talvez seja a mais simples de visualizar como programador: cada core executando uma determinada tarefa. Nesse ponto, é importante considerar a diferença entre um modelo de memória distribuído e compartilhado:

No modelo de memória compartilhada, o que acontece quando você tem múltiplos processos acessando a mesma área de memória ao mesmo tempo? Sim, é isso mesmo: race conditions. Vou ilustrar:

Qual o antídoto para o caos? Ordem. Porém, é um pouco difícil adicionar thread safety somente usando locks. Esse modelo acaba sendo mais simples até um determinado ponto, afinal está tudo na mesma máquina, certo?

Porém, o que acontece quando você precisa escalar para uma carga que somente uma máquina (ou um core) não é mais o suficiente? enter the distributed memory model.

No modelo de memória distribuída, toda o acesso a memória é feito através de um conceito que eu acho incrível que se chama troca de mensagens. Eu sei, o nome é meio bobo, mas é o que torna o actor model possível. Alguns usos desse modelo que eu consigo pensar são: a linguagem Erlang em sua máquina virtual BEAM, a maravilhosa linguagem Elixir (além de brasileira, é puro amor) e o framework Akka, disponível em .NET e Java. Essa técnica resolve vários problemas de escalabilidade de aplicações, por permitir a comunicação entre múltiplos cores, mesmo que eles não estejam na mesma máquina.

Para finalizar: qual o uso de tudo isso e o impacto na sua carreira de desenvolvedor? Concorrência é a chave para sistemas responsivos, eficientes e tolerante a falhas.

Sabe porque seu sistema operacional não fica mais “pensando” quando tá abrindo um programa e travava tudo? Porque ele tá lidando com múltiplos programas ao mesmo tempo (e usando os múltiplos cores para fazer isso de forma eficiente). A thread principal (UI Thread) mantém a sua tela responsiva enquanto uma background thread faz o processamento do que quer que seja necessário.

Em seu famoso artigo “Free Lunch is Over”, Herb Stutter identificou o fim da Lei de Moore pois estávamos chegando aos limites físicos dos transistores. Se isso não tivesse acontecido, teríamos CPUs de 10Ghz hoje em dia se a tendência tivesse se mantido. Portanto, a tendência é algo mais ou menos assim desde então:

Mais cores ao invés de mais hertz. Essa tendência se mostrou verdade (o artigo original é de 2005!) que que algumas pessoas se inspiraram e fizerem algo a respeito disso. Sério, estudem Elixir (é a linguagem mais promissora na minha opinião dentro desse tópico).

Qual o ponto de tudo isso? Nenhum.

Eu só estava querendo aprender esses tópicos com mais profundidade e vou compartilhando o que eu descobri por aqui mesmo :)

No próximo artigo, vamos falar sobre threads e locks.

Até!

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

--

--

Adriano Croco
Adriano Croco

No responses yet