MÓDULO 04 · CONCEITO 01 DE 14

Concorrência vs paralelismo

A distinção que organiza todo o pensamento sobre execução de múltiplas coisas. Dijkstra, Lamport, Pike — e por que confundir os dois custa caro em decisões arquiteturais.

Tempo de leitura ~22 min Pré-requisito Módulo 01 (processos & threads) Próximo Threads, processos e o modelo do SO

Em outubro de 2012, Rob Pike subiu ao palco do Heroku Waza com uma palestra que virou referência canônica em uma área cheia de confusão terminológica. O título era simples e provocativo: Concurrency is not Parallelism. Pike, um dos criadores de Go e veterano da Bell Labs onde Plan 9 e UTF-8 nasceram, gastou quarenta minutos defendendo uma distinção que para muitos parecia preciosismo acadêmico. Não era. A confusão entre concorrência e paralelismo é responsável por uma fração desproporcional das decisões erradas em sistemas concorrentes — desde "vamos colocar threads para ficar mais rápido" até "microsserviços resolvem nossa lentidão".

A confusão tem origem histórica. Nos anos 1960 e 1970, quando os primeiros sistemas concorrentes apareceram em mainframes da IBM e DEC, paralelismo real era raro: máquinas com vários processadores eram exóticas, caras, e usadas em laboratórios. Os primeiros sistemas operacionais multi-tarefa, multiprogramming e time-sharing executavam vários processos em uma só CPU, alternando entre eles. Concorrência e paralelismo coincidiam tão raramente que os termos eram usados como sinônimos na literatura prática. Esse vazamento conceitual atravessou décadas e ainda contamina conversas em 2026, mesmo com cada smartphone tendo oito núcleos.

Hoje, a distinção é central porque cada modelo de programação concorrente faz uma escolha distinta sobre se também é paralelo. Async/await em Python é concorrente e não paralelo (single thread cooperativo). Goroutines em Go são concorrentes e podem ser paralelas (scheduler M:N distribui em vários núcleos do SO). Threads em Java sob Loom 21+ são concorrentes e podem ser paralelas, mas com semântica de virtual threads. Multiprocessing em qualquer linguagem é paralelo por construção. Sem o vocabulário certo, esses modelos viram um borrão indistinguível, e decisões arquiteturais são tomadas no escuro.

Este conceito não ensina a escrever código concorrente — isso vem nos treze conceitos seguintes. Aqui se constrói o vocabulário e o modelo mental que organizam tudo o que vem depois. Sem esse alicerce, async, goroutines, locks e channels viram tópicos isolados; com ele, viram variações disciplinadas do mesmo princípio.

A distinção em duas frases

A definição de Pike é a melhor síntese disponível, e cabe em duas sentenças:

a frase canônica

Concurrency is dealing with lots of things at once. Parallelism is doing lots of things at once. — Rob Pike, 2012.

Concorrência é uma propriedade da estrutura de um programa: a forma como ele decompõe a execução em fluxos independentes que podem progredir intercaladamente. É decisão de design — você escreve um programa concorrente. Paralelismo é uma propriedade da execução: dois ou mais fluxos rodando literalmente ao mesmo tempo, em hardware distinto. É decisão de runtime — o sistema operacional, o scheduler da linguagem ou o kernel decidem se vão de fato paralelizar o que você escreveu como concorrente.

A consequência mais útil desta definição: você pode ter concorrência sem paralelismo, e pode ter paralelismo sem precisar pensar em concorrência. Um programa Python com asyncio é altamente concorrente — orquestra centenas de tarefas — e jamais executa duas em paralelo, porque o event loop é single-thread. Um programa que aplica uma operação SIMD a um vetor é paralelo (a CPU executa a mesma instrução em vários dados ao mesmo tempo) e o programador não escreveu nada concorrente — apenas um loop. Os dois conceitos vivem em planos distintos.

A analogia do barista

Pike usou uma analogia que se firmou no folclore da área. Imagine uma cafeteria com vários pedidos chegando ao mesmo tempo. Cada pedido envolve várias etapas: moer grão, extrair espresso, vaporizar leite, montar a bebida, chamar o cliente. Há duas formas distintas de organizar o trabalho:

Um único barista pode atender vários pedidos concorrentemente: enquanto a máquina extrai o espresso de um pedido (espera passiva), ele inicia a moagem do próximo, e assim por diante. Há um único trabalhador, mas vários pedidos progridem ao mesmo tempo, intercalando atividades quando uma delas é forçada a esperar. Isso é concorrência sem paralelismo. Não há ganho de força bruta — o barista não tem mais mãos — mas há ganho de tempo total porque ninguém fica parado vendo a máquina operar.

Dois baristas trabalhando em pedidos diferentes ao mesmo tempo é paralelismo. Há literalmente duas execuções acontecendo em hardware distinto. O ganho é proporcional ao número de baristas, até o ponto em que algum recurso compartilhado vira gargalo (uma única máquina de café limitando os dois). Note que paralelismo aqui depende de o trabalho ter sido dividido com sucesso — se houvesse um pedido só, dois baristas não fariam dele mais rápido senão coordenando passos com algum custo.

A analogia esconde uma sutileza importante: concorrência é o que você organiza; paralelismo é o que aparece quando há recursos. Um sistema bem decomposto em tarefas concorrentes vira automaticamente paralelo se a runtime tiver cores disponíveis. Um sistema mal decomposto não vira paralelo nem com cores sobrando.

A matriz cores × fluxos

Para fixar a distinção, vale percorrer as quatro combinações possíveis entre número de cores e número de fluxos de execução. Cada uma corresponde a uma situação real, e algumas só existem como relíquia histórica.

Single-core, single-thread

Programa estritamente sequencial em uma CPU sem mais nada. Sem concorrência, sem paralelismo. É o modelo de um script de shell simples ou de um programa CLI tradicional. Cada instrução é executada uma após a outra; bugs de concorrência são impossíveis porque não há concorrência. Hoje quase ninguém roda em hardware assim, mas o modelo é útil como ponto de partida — qualquer complicação que você adicionar terá que justificar o custo.

Single-core, multi-thread (ou múltiplos fluxos)

Concorrência sem paralelismo de fato. O scheduler do sistema operacional alterna entre threads em fatias de tempo (preempção em Linux, Windows, macOS) ou em pontos de yield (cooperativa em asyncio, JavaScript). O programador vê o efeito de paralelismo — vários fluxos progredindo — mas o hardware executa um só de cada vez. Toda a problemática de concorrência aparece aqui: races, deadlocks, ordering, visibility. O paralelismo não, porque ele não existe.

Multi-core, single-thread

Hardware capaz de paralelo, software incapaz de usá-lo. É o que acontece quando se roda um script Python sequencial em um laptop de oito cores: sete cores ficam ociosos. Computação científica em Python sem multiprocessing é o exemplo clássico — gente reclama que "Python é lento" quando o que está acontecendo é uso de um oitavo do hardware.

Multi-core, multi-thread

O caso interessante. Aqui o programa pode ser paralelo, e depende da implementação da runtime saber distribuir threads em cores. Em Go, o runtime faz isso por padrão (GOMAXPROCS iguala o número de cores lógicos). Em C#, o ThreadPool usa todos os cores. Em Java, o ForkJoinPool faz o mesmo. Em Python, mesmo com várias threads, o GIL serializa execução de bytecode — concorrência existe, paralelismo de CPU não. Isso é uma das peculiaridades mais consequentes do ecossistema Python e merece tratamento próprio.

O caso Python: GIL e o que ele complica

O Global Interpreter Lock — o famoso GIL — é um mutex no interpretador CPython que garante que apenas uma thread execute bytecode Python por vez. A razão é histórica e prática: simplifica drasticamente o gerenciamento de memória do interpretador (reference counting sem locks finos), e permite que C extensions assumam single-threadedness. O efeito colateral é que threading em Python não dá paralelismo de CPU. Quatro threads pesadas em cálculo bruto não rodam mais rápido que uma — frequentemente rodam mais devagar, porque há overhead de troca de contexto sem ganho.

Há três caminhos para contornar essa limitação. asyncio funciona porque é cooperativo e single-thread por design — não tenta paralelizar nada, usa concorrência para mascarar latência de I/O. multiprocessing dispara processos separados, cada um com seu próprio interpretador e seu próprio GIL; comunicação acontece via IPC (pickle + pipes ou shared memory). C extensions podem soltar o GIL em código nativo (NumPy, PyTorch, e Cython com nogil fazem isso onde possível). Python 3.13, lançado em outubro de 2024, introduziu uma flag experimental --disable-gil que torna o GIL opcional; o ecossistema ainda está se adaptando, e em 2026 a maioria das libs científicas ainda assume GIL.

Em C#, Go, Rust, Java e a maioria das linguagens compiladas, threads são paralelas de fato em multi-core. A distinção entre concorrência e paralelismo nelas é mais limpa: concorrência é o modelo de programação, paralelismo é o que a runtime entrega. Em Python, a distinção é mais sutil porque a entrega depende do tipo de trabalho (I/O-bound aproveita threading; CPU-bound não).

armadilha clássica

threading.Thread em Python para acelerar trabalho CPU-bound. O programador adiciona quatro threads, mede, descobre que ficou igual ou mais lento, e culpa "Python lento" sem entender o GIL. Para CPU-bound em Python, use multiprocessing, libs com nogil (NumPy/SciPy/Polars) ou avalie Python 3.13 free-threaded com cuidado. Para I/O-bound, threading e asyncio funcionam ambos — asyncio costuma ser mais barato em centenas de tarefas.

A tradição teórica — Dijkstra, Hoare, Lamport

A distinção entre concorrência e paralelismo precede qualquer linguagem moderna. Edsger Dijkstra escreveu, em 1965, um texto curto chamado Cooperating Sequential Processes, distribuído como manuscrito em Eindhoven antes de virar artigo formal em 1968. Ali Dijkstra define o problema: como múltiplos processos sequenciais podem cooperar sem se atropelar quando compartilham recursos? O texto introduz semáforos, e o conceito de processo sequencial se torna o átomo da discussão. Note: Dijkstra fala de processos como abstração de programação. Se eles vão executar em paralelo de fato é questão da implementação — mas a estrutura concorrente já existe no papel.

Tony Hoare publicou em 1978 Communicating Sequential Processes, onde propõe um modelo radical: processos não compartilham memória, comunicam-se por troca de mensagens em canais síncronos. CSP virou base teórica de Go (via Pike e Thompson, que conheciam Hoare desde Bell Labs) e de Erlang (via Joe Armstrong, ainda que Erlang seja mais aderente ao actor model de Hewitt). A relevância para esta discussão: CSP é uma teoria de concorrência puramente estrutural, sem assumir paralelismo. Você pode ter um sistema CSP perfeitamente válido executando sequencialmente — só não tira proveito do hardware.

Leslie Lamport publicou no mesmo ano de 1978 um artigo que mudou a forma como se pensa em concorrência distribuída: Time, Clocks, and the Ordering of Events in a Distributed System. Ali Lamport argumenta que em sistemas concorrentes — distribuídos ou não — a noção de "antes" e "depois" precisa de redefinição. A relação happens-before que Lamport definiu é a fundação dos memory models modernos (Java, C++, Go) e voltará no conceito 10. Por ora, o ponto é: concorrência tem teoria própria, anterior e independente de paralelismo.

O que cada modelo te dá — uma escala

Modelos de execução podem ser organizados numa escala de concorrência crescente, paralelismo crescente, ou ambos. Os mais comuns:

  1. Síncrono single-thread: nem concorrência nem paralelismo. Programa simples, fácil de raciocinar.
  2. Cooperativo single-thread (asyncio, JS event loop, Tokio em Rust com 1 worker): concorrência, sem paralelismo. Excelente para I/O. Inútil para CPU-bound.
  3. Preemptivo multi-thread em multi-core (.NET ThreadPool, JVM, Go runtime): concorrência e paralelismo juntos. Modelo dominante em backend de aplicação.
  4. Multi-process (multiprocessing, container replicas): paralelismo com isolamento de memória; comunicação custosa via IPC ou rede.
  5. SIMD / GPU: paralelismo massivo, restringido a problemas que cabem na forma do hardware (mesma operação em muitos dados).
  6. Distribuído: concorrência entre nós; cada nó pode ser paralelo internamente. Tema do módulo 09.

Cada nível tem custo de coordenação maior que o anterior. Migrar de single-thread para cooperativo custa pouco (refatorar para async). Migrar para multi-thread custa mais (locks, atomicity). Migrar para multi-process custa muito (IPC, serialização). Migrar para distribuído custa caríssimo (latência de rede, falhas parciais, consenso). Um princípio sobrevivente: só suba na escala quando o nível atual virar gargalo medido — não por antecipação.

O mesmo programa nas três linguagens

Para concretizar a distinção, considere o problema clássico: buscar cinco URLs em paralelo. É I/O-bound, então concorrência sem paralelismo já resolve a maior parte. Veja como cada linguagem expressa a estrutura concorrente, e o que a runtime faz com ela.

C# — Task.WhenAll (concorrente, pode ser paralelo)
using var http = new HttpClient();
var urls = new[] {
  "https://example.com/a", "https://example.com/b",
  "https://example.com/c", "https://example.com/d",
  "https://example.com/e"
};

var tarefas = urls.Select(async url => {
    var resp = await http.GetStringAsync(url);
    return (url, resp.Length);
});

var resultados = await Task.WhenAll(tarefas);

foreach (var (url, len) in resultados)
    Console.WriteLine($"{url}: {len} bytes");

Task.WhenAll orquestra cinco tarefas concorrentes. Cada await libera a thread atual ao ThreadPool. Em multi-core, várias respostas podem ser processadas simultaneamente em threads distintas — concorrência aqui vira paralelismo na prática. A decisão é da runtime, não do código.

Python — asyncio.gather (concorrente, NÃO paralelo)
import asyncio
import aiohttp

URLS = [
    "https://example.com/a", "https://example.com/b",
    "https://example.com/c", "https://example.com/d",
    "https://example.com/e",
]

async def fetch(session, url):
    async with session.get(url) as resp:
        body = await resp.text()
        return url, len(body)

async def main():
    async with aiohttp.ClientSession() as session:
        tarefas = [fetch(session, u) for u in URLS]
        for url, n in await asyncio.gather(*tarefas):
            print(f"{url}: {n} bytes")

asyncio.run(main())

asyncio.gather roda cinco corotinas concorrentemente em uma única thread via event loop cooperativo. Não há paralelismo de CPU em jogo — quando uma tarefa espera I/O, a thread roda outra. Para I/O-bound isso é ideal. Para CPU-bound seria inútil.

Go — goroutines + WaitGroup (concorrente E paralelo)
package main

import (
    "fmt"
    "io"
    "net/http"
    "sync"
)

func main() {
    urls := []string{
        "https://example.com/a", "https://example.com/b",
        "https://example.com/c", "https://example.com/d",
        "https://example.com/e",
    }
    var wg sync.WaitGroup
    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            resp, err := http.Get(u)
            if err != nil { return }
            defer resp.Body.Close()
            body, _ := io.ReadAll(resp.Body)
            fmt.Printf("%s: %d bytes\n", u, len(body))
        }(url)
    }
    wg.Wait()
}

Cinco goroutines disparadas. O scheduler M:N do runtime do Go distribui em GOMAXPROCS threads do SO (por padrão, número de cores lógicos). Em uma máquina de oito cores, várias requisições podem estar literalmente sendo parseadas em paralelo. Concorrência estrutural; paralelismo emergente.

Granularidade — onde você opera

Concorrência e paralelismo aparecem em escalas muito diferentes dentro de um mesmo sistema, e cada escala tem ferramentas próprias. Entender em que granularidade você está operando muda quais problemas importam.

Granularidade fina — instrução e dado

No nível mais baixo, CPUs modernas executam várias instruções por ciclo via instruction-level parallelism (pipelining, out-of-order execution, superscalar). Vetorização SIMD aplica a mesma operação a múltiplos dados em uma instrução (AVX-512 opera em 16 floats por vez). Compiladores modernos (LLVM, GCC, .NET RyuJIT) fazem boa parte disso automaticamente. O programador raramente toca aqui — exceto em computação numérica intensiva, onde escolher tipos e layouts amigáveis a vetorização vira diferença de performance grande.

Granularidade média — função e tarefa

A escala onde a maior parte do código de aplicação opera. Uma goroutine, uma Task, uma corotina async, uma thread — todas representam unidades de trabalho do tamanho de "uma função ou um pequeno conjunto de funções". Custos e ferramentas conhecidos: locks, channels, queues, async/await. Os treze conceitos restantes deste módulo vivem aqui.

Granularidade grossa — processo, container, serviço

Um processo separado, um container Kubernetes, um microsserviço. Comunicação é cara: serialização, rede, latência. Coordenação é muito mais difícil — não há memória compartilhada, falhas parciais são realidade, latência domina. As ferramentas são outras: gRPC, mensageria, consenso distribuído. Esse é o tema do módulo 09 e dos módulos seguintes sobre escalabilidade e resiliência.

Erros de granularidade são frequentes. Tratar microsserviços como threads (esperar latência de microssegundos) é uma das fontes de sistemas que parecem rápidos no laboratório e arrastam em produção. Tratar threads como microsserviços (assumir que falham e podem ser reiniciadas independentemente) leva a designs onde estado fica perdido em qualquer hiccup.

heurística do sênior

Antes de paralelizar qualquer coisa, meça e classifique. A pergunta "este trabalho é I/O-bound ou CPU-bound?" decide o modelo certo. Se a maior parte do tempo é esperando rede, banco ou disco, concorrência cooperativa (asyncio, async/await) já resolve sem precisar de threads ou processos. Se a maior parte é cálculo bruto, você precisa de paralelismo de fato — e em Python isso significa multiprocessing ou nogil.

Por que essa distinção importa para sua carreira

Em entrevistas técnicas para vagas sêniores, "qual a diferença entre concorrência e paralelismo?" é uma das perguntas mais comuns, e a resposta separa quem leu o termo duas vezes de quem entendeu. A resposta forte não é só citar Pike — é articular: "concorrência é decisão de design, paralelismo é decisão de runtime; um programa pode ser muito concorrente sem ser paralelo (asyncio em Python), pode ser paralelo sem precisar de modelo concorrente sofisticado (SIMD), e em multi-core moderno o desejável é decompor de forma concorrente para que a runtime entregue paralelismo de bônus." Em design de sistemas, a mesma distinção decide se você escolhe um modelo cooperativo (Node, asyncio), threads tradicionais (Java pré-Loom, C# ThreadPool), goroutines, multiprocessing ou distribuição. Cada escolha é apropriada em alguma situação e catastrófica em outra.

Como praticar

  1. Reescreva o mesmo programa em três modelos. Pegue algo simples — fazer dez requisições HTTP a endpoints lentos (use httpbin.org/delay/1) e somar tamanhos das respostas. Implemente síncrono, com asyncio, com threading e com multiprocessing em Python. Meça os tempos. Escreva uma frase para cada versão explicando por que o tempo é o que é.
  2. Demonstre o GIL para si mesmo. Crie uma função CPU-bound (ex.: contar números primos até N=10⁷). Execute sequencialmente, depois em quatro threads, depois em quatro processos. Compare os tempos e plote num gráfico. A diferença entre threading e multiprocessing aqui é a explicação mais clara do GIL que você pode dar a alguém.
  3. Experimente o limite de paralelismo. Em Go ou C#, escreva um programa que soma todos os números de 1 a 10⁹ dividindo o intervalo entre N workers. Varie N de 1, 2, 4, 8, 16, 32 e meça o tempo. Plote o speedup. Identifique onde adicionar worker para de ajudar — esse é o ponto onde Amdahl, contention de memória ou cache trashing começam a dominar. Saber explicar esse gráfico é diferenciador real.

Referências para aprofundar

  1. livro The Art of Multiprocessor Programming — Maurice Herlihy & Nir Shavit (2ª ed., 2020). O tratamento mais rigoroso de concorrência em livro técnico. Vai dos fundamentos teóricos (linearizability, lock-free) até implementações práticas. Denso, mas indispensável.
  2. livro Concurrency in Go — Katherine Cox-Buday (2017). A entrada acessível para CSP via Go. Os primeiros dois capítulos cobrem concorrência vs paralelismo com clareza prática rara.
  3. livro Operating Systems: Three Easy Pieces — Remzi & Andrea Arpaci-Dusseau (gratuito online). pages.cs.wisc.edu/~remzi/OSTEP — Parte II inteira é sobre concorrência. Didática impecável, exercícios bons, atualizado.
  4. livro Designing Data-Intensive Applications — Martin Kleppmann (2017). Cap. 7 (Transactions) e Cap. 8 (Trouble with Distributed Systems) tratam concorrência de forma que conecta com bancos e sistemas distribuídos.
  5. artigo Concurrency is not Parallelism — Rob Pike, transcrição da palestra (2012). go.dev/blog/waza-talk — Pike argumenta a distinção via gophers movendo livros. Quinze minutos de leitura, mudança de modelo mental permanente.
  6. artigo What Color is Your Function? — Bob Nystrom (2015). journal.stuffwithstuff.com — ensaio clássico sobre o "problema da cor" em async/await, que toca diretamente na confusão entre concorrência e paralelismo.
  7. artigo Notes on Structured Concurrency, or: Go statement considered harmful — Nathaniel J. Smith (2018). vorpus.org — argumento influente que originou structured concurrency em Trio (Python). Distinção entre concorrência estruturada e não-estruturada vai aparecer no conceito 05.
  8. paper Time, Clocks, and the Ordering of Events in a Distributed System — Leslie Lamport (1978). microsoft.com/en-us/research/publication/time-clocks-ordering-events-distributed-system — Premiado Turing. Onze páginas que fundaram memory models e consenso distribuído.
  9. paper Cooperating Sequential Processes — Edsger W. Dijkstra (1965/1968). cs.utexas.edu/~EWD/transcriptions/EWD01xx/EWD123 — Manuscrito original de Dijkstra. Define o problema de processos cooperativos e introduz semáforos.
  10. docs Python — asyncio. docs.python.org/3/library/asyncio.html — A documentação melhorou muito desde 3.10. Lê-se em uma tarde e cobre o modelo cooperativo inteiro.
  11. docs The Go Memory Model. go.dev/ref/mem — Sintética e precisa. Define formalmente happens-before em Go. Vai ser referência no conceito 10, mas vale ler agora.
  12. vídeo Concurrency is not Parallelism — Rob Pike (Heroku Waza, 2012). YouTube. Quarenta minutos. A palestra original com slides dos gophers. Vale assistir antes mesmo de ler a transcrição — Pike é didático e bem-humorado.