Paralelismo X Concorrência

Matheus Kielkowski
5 min readOct 21, 2023

No universo da computação, conforme as demandas por eficiência e velocidade cresceram, surgiram abordagens diferentes para maximizar o uso dos recursos de hardware e lidar com múltiplas tarefas simultaneamente. Duas dessas abordagens, frequentemente mencionadas, são o “Paralelismo” e a “Concorrência”. Embora esses termos sejam por vezes usados de formas parecidas, eles têm nuances distintas e se referem a conceitos ligeiramente diferentes.

O que é Paralelismo?

O paralelismo envolve a execução simultânea de tarefas, geralmente dividindo um problema maior em partes menores que podem ser processadas ao mesmo tempo. Isso é frequentemente visto em arquiteturas de hardware com múltiplos núcleos ou processadores, onde cada núcleo pode trabalhar em uma parte diferente de uma tarefa maior.

O paralelismo é frequentemente implementado no nível de hardware. Processadores modernos são muitas vezes multi-core, o que significa que eles têm vários núcleos de processamento que podem executar tarefas simultaneamente. Além disso, em sistemas de alto desempenho, várias CPUs ou máquinas podem trabalhar juntas em paralelo.

O principal benefício do paralelismo é a capacidade de resolver problemas maiores e mais complexos em menos tempo. Ele é fundamental em campos como a pesquisa científica, a análise de grandes volumes de dados e a renderização gráfica, onde a capacidade de processar informações de maneira rápida é essencial.

O que é Concorrência?

Concorrência é um conceito na ciência da computação que se refere à habilidade de um sistema em gerenciar múltiplas tarefas ou processos de forma que pareçam estar ocorrendo simultaneamente. Embora frequentemente associada ao paralelismo, a concorrência não é estritamente sobre a execução simultânea de tarefas. Em vez disso, é sobre a estruturação e organização para lidar com muitas coisas ao mesmo tempo.

Em sistemas operacionais, a concorrência é frequentemente implementada através da multitarefa, onde o sistema alterna rapidamente entre tarefas para dar a impressão de que todas estão sendo executadas ao mesmo tempo, mas por de baixo dos panos, todas estão sendo gerenciadas e processadas por apenas um núcleo.

Enquanto a concorrência está relacionada ao design de sistemas que lidam com muitas tarefas, o paralelismo refere-se à execução simultânea de operações. Um sistema pode ser concorrente sem ser paralelo, e vice-versa.

Mas na prática, qual a diferença?

Vamos utilizar a linguagem Go para exemplificar como aplicariamos o uso do paralelismo e concorrência na programação. Visto que Go foi projetado e é muito famoso por esta capacidade de podermos trabalhar com os dois tipos de paradigmas.

Se estivessémos querendo desenvolver um código que funcione de forma paralela, poderiamos usar o seguinte exemplo:

package main

import (
"fmt"
"time"
)

func worker(id int, ch chan int) {
fmt.Printf("Worker %d starting\n", id)

// Simulando algum trabalho pesado
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)

ch <- id
}

func main() {
// Criando um canal para comunicar resultados
ch := make(chan int)

// Iniciando três goroutines
for i := 0; i < 3; i++ {
go worker(i, ch)
}

// Aguardando os resultados das goroutines
for i := 0; i < 3; i++ {
result := <-ch
fmt.Printf("Received result from worker %d\n", result)
}
}

// Resultado:

// Worker 2 starting
// Worker 0 starting
// Worker 1 starting

// Worker 0 done
// Received result from worker 0

// Worker 2 done
// Received result from worker 2

// Worker 1 done
// Received result from worker 1

Neste exemplo, nosso código teve o seguinte comportamento:

  • Criamos uma função worker que simula algum trabalho pesado, dormindo por um segundo.
  • Na função main, iniciamos três dessas funções worker como goroutines usando a palavra-chave go.
  • O canal ch é usado para comunicação entre a main goroutine e as worker goroutines. As workers enviam seus IDs para o canal quando terminam seu trabalho.
  • A main goroutine aguarda os resultados de todas as worker goroutines lendo do canal três vezes.

Caso o nosso sistema tenha mais do que um núcleo, as goroutines serão distribuídas para cada núcleo, e não gerenciadas por apenas uma única unidade de processamento, ou como costumam dizer, thread.

Mas e se precisassemos trabalhar com a concorrência entre os processos em um sistema single-threaded (apenas uma threaded), como fariamos?

Vamos ver um exemplo simples de concorrência em Go usando goroutines e canais:

package main

import (
"fmt"
"time"
)

func printNumbers(ch chan bool) {
for i := 0; i < 5; i++ {
time.Sleep(250 * time.Millisecond)
fmt.Printf("%d ", i)
}
ch <- true
}

func printLetters(ch chan bool) {
for i := 'a'; i < 'f'; i++ {
time.Sleep(400 * time.Millisecond)
fmt.Printf("%c ", i)
}
ch <- true
}

func main() {
ch1 := make(chan bool)
ch2 := make(chan bool)

go printNumbers(ch1)
go printLetters(ch2)

<-ch1
<-ch2
}

// Resultado:
// 0 a 1 2 b 3 c 4 d e

Neste exemplo:

  • Temos duas funções, printNumbers e printLetters, que imprimem números e letras, respectivamente, com diferentes intervalos de tempo.
  • Ambas as funções são iniciadas como goroutines na função main.
  • Cada função envia um valor para seu canal correspondente após concluir sua tarefa.
  • Na função main, esperamos que ambas as goroutines concluam antes de finalizar o programa, lendo de ambos os canais.

Para executar os códigos, acesse o link: https://go.dev/play/

Mas então, na prática os dois são a mesma coisa?

Não. Concorrência e Paralelismo são parecidos, mas de fato não se engane, pois não são a mesma coisa.

Vamos imaginar que você tem duas tarefas, ler e-mails e ouvir música.

Na Concorrência é como ter ambas as tarefas à sua frente e você alternar entre ler alguns e-mails e se concentrar na música, isso causa a sensação de você estar fazendo as duas tarefas de forma simultânea, mas na realidade você está trocando entre elas, sem que ao menos perceba se não prestar atenção.

Já no Paralelismo, é como se duas pessoas estivessem realizando as tarefas ao mesmo tempo, uma lendo e-mails e a outra ouvindo música. Ambas as tarefas estão sendo executadas simultaneamente, mas por “núcleos” (pessoas) diferentes.

Em Go, temos as Goroutines para trabalhar com concorrência, elas são leves e podem ter milhares delas rodando concorrentemente. Mas, se estiver em um sistema de um único núcleo, essas goroutines não serão realmente executadas em paralelo. Em vez disso, o runtime de Go alternará entre elas para dar a aparência de simultaneidade.

E no Paralelismo Real, precisamos de sistemas com múltiplos núcleos, pois assim as goroutines podem ser executadas em núcleos diferentes simultaneamente.

O runtime de Go gerencia isso por padrão, escalando de acordo com a quantidade de núcleos disponíveis.

Por fim…

A evolução das necessidades computacionais e o crescimento explosivo dos dados têm levado a indústria da computação a buscar maneiras mais eficientes de processar informações. Neste contexto, os conceitos de paralelismo e concorrência surgem como abordagens cruciais para otimizar a execução de tarefas e aproveitar ao máximo o hardware disponível.

Tanto a concorrência quanto o paralelismo são ferramentas vitais da computação moderna. Eles atendem a diferentes necessidades e apresentam seus próprios conjuntos de desafios. Compreender as nuances entre eles e saber quando e como aplicar cada abordagem é fundamental para desenvolver sistemas eficientes e escaláveis no cenário tecnológico atual.

Espero que este breve artigo tenha clareado suas ideias, e o ajude a saber quais os caminhos caso queira se aprofundar no tema. 😉

--

--

Matheus Kielkowski
Matheus Kielkowski

Written by Matheus Kielkowski

Software Enginner in love with web technologies 👨‍💻

No responses yet