Em agosto de 1978, a revista Communications of the ACM publicou um artigo de quinze páginas de Tony Hoare intitulado Communicating Sequential Processes. Hoare, que já tinha proposto o algoritmo de Quicksort e o conceito de invariantes monitoradas, propunha algo diferente: uma teoria formal em que programas concorrentes seriam expressos como processos sequenciais que não compartilham memória, comunicando-se exclusivamente por troca de mensagens em canais síncronos. Para Hoare, eliminar memória compartilhada eliminava boa parte da complexidade de concorrência — sem estado partilhado, não há condição de corrida; o que se preocupar é apenas com a coreografia das mensagens.
O modelo CSP virou a base teórica de várias linguagens experimentais: Occam (Inmos, 1983), Limbo (Plan 9, 1995), e eventualmente Go. Em 2007, Rob Pike, Ken Thompson e Robert Griesemer começaram a esboçar uma linguagem nova no Google. Pike e Thompson conheciam Hoare desde os anos da Bell Labs; ambos haviam trabalhado em Plan 9 e Limbo, onde CSP era primitiva nativa. Em novembro de 2009, Go foi anunciada publicamente. A primitiva central, herdada diretamente de CSP, era a goroutine — um processo sequencial leve que comunica com outros via channels.
A frase que Pike popularizou — "Don't communicate by sharing memory; share memory by communicating" — virou slogan da linguagem e resumo da filosofia. Em 2026, Go é uma das três linguagens mais usadas em backend de cloud (junto a Java e Python), e CSP saiu do nicho acadêmico para infraestrutura de produção em escala global: Kubernetes, Docker, Cloudflare, Cockroach, Influx, Grafana — todos escritos em Go.
Este conceito mostra como Go materializa CSP. Goroutines como
processos sequenciais; channels como canais de comunicação;
select como multiplexador; o scheduler M:N que
torna milhões de goroutines viáveis. Inclui também breve
contraste com o actor model (Hewitt, 1973), que parece similar
mas tem semântica importante diferente — diferença que vai
aparecer no próximo conceito.
O modelo CSP — em três frases
CSP, na forma como Hoare formulou e Go encarnou, cabe em três ideias:
- Processos sequenciais: cada fluxo de execução é sequencial dentro de si mesmo. Concorrência aparece apenas na composição entre processos.
- Canais como cidadãos primários: comunicação acontece via canais com identidade explícita. Canais têm tipo, capacidade, e operações send e receive.
- Sincronização emergente: a comunicação em um canal síncrono é, por construção, um ponto de sincronização. Um send espera um receive, ou vice-versa. Coordenação não precisa de mutex separado.
Compare com o modelo de threads tradicionais (visto no conceito 02) e com o modelo de async/await (conceito 04). Threads tradicionais compartilham memória e sincronizam com mutex e condition variables. Async/await faz concorrência cooperativa em única thread, com estado compartilhado por padrão. CSP isola estado entre processos e usa canais como única ponte. Cada modelo é apropriado em contextos diferentes; CSP brilha em sistemas com fluxos paralelos bem-definidos que precisam coordenar mas raramente compartilhar estado mutável.
Goroutines — processos sequenciais leves
Goroutine é a primitiva de Go para criar um processo sequencial
no sentido CSP. Sintaticamente, basta colocar go
antes de uma chamada de função:
func saudar(nome string) {
fmt.Println("olá,", nome)
}
func main() {
go saudar("Camila") // dispara goroutine; main continua
go saudar("Bia")
time.Sleep(100 * time.Millisecond) // espera goroutines (placeholder)
}
A função roda concorrentemente; main não bloqueia
esperando. Se main retornar antes das goroutines
terminarem, o programa encerra junto — goroutines não são
threads daemon que sobrevivem ao processo. Em programa real,
sync.WaitGroup ou um channel resolve a coordenação,
como veremos.
O que torna goroutines especiais é o custo. O conceito 02 já apresentou os números: stack inicial de 2 KB (vs 8 MB de uma thread Linux), criação em centenas de nanossegundos, milhões de goroutines em uma máquina sem desconforto. Mas a parte mais interessante é como o runtime de Go entrega esse modelo — através de um scheduler M:N em user-space que merece um olhar próprio.
O scheduler M:N — modelo G-M-P
Dmitry Vyukov, contribuidor central do scheduler de Go, publicou em 2012 um design document que detalha como goroutines são distribuídas em threads do sistema operacional. O modelo tem três entidades:
- G (goroutine): a unidade de trabalho criada pelo programador. Tem stack próprio, program counter, estado.
- M (machine): uma kernel thread. M é onde a execução de fato acontece, no nível do SO.
-
P (processor): um "logical processor" do
runtime. P mantém uma fila local de goroutines runnable. O
número de P é definido por
GOMAXPROCS(default: número de cores lógicos da máquina).
Para uma goroutine executar, ela precisa estar associada a um P,
que por sua vez está associado a uma M. O scheduler do runtime
faz essa cola: distribui goroutines runnable em P, e P em M.
Quando uma goroutine bloqueia (em syscall, em canal, em
time.Sleep), o runtime desassocia: a M continua,
mas a P pode ser tomada por outra M ociosa, mantendo
paralelismo.
Work stealing
Quando uma P fica sem goroutines em sua fila local, ela "rouba" goroutines de outras P — mecanismo conhecido como work stealing, desenhado originalmente para Cilk em MIT (1995) e adotado em quase todos os schedulers de goroutine-like modernos (Erlang, Java ForkJoinPool, Tokio, .NET ThreadPool desde 4.0). O efeito é balanceamento dinâmico sem coordenação central — cada P toma decisões locais.
Preempção
Até Go 1.14 (fev/2020), o scheduler era estritamente
cooperativo: goroutines cediam apenas em pontos seguros
(chamadas de função, alocação, comunicação em channel).
Goroutine que entrasse em loop apertado sem chamar nada
bloquearia uma P inteira, podendo causar starvation. Go 1.14
introduziu preempção baseada em sinais — o runtime dispara
SIGURG em goroutines que rodam por muito tempo
sem ceder, forçando-as a parar. Em 2026, o scheduler é
híbrido cooperativo-preemptivo, com latência tail melhor que
no modelo puramente cooperativo.
Channels — comunicação como primitiva
Channel é a primitiva de comunicação de CSP em Go. Tem tipo
estático, capacidade definida na criação, e operações
send (ch <- v) e receive
(v := <-ch). A semântica depende da capacidade.
Unbuffered channels — síncronos
ch := make(chan int) cria channel de capacidade
zero. Send bloqueia até alguma goroutine fazer receive; receive
bloqueia até alguma goroutine fazer send. O efeito é
sincronização: a comunicação acontece quando ambos os lados se
encontram. É a forma mais próxima do CSP original de Hoare.
ch := make(chan string)
go func() {
ch <- "olá" // bloqueia até alguém receber
}()
msg := <-ch // bloqueia até alguém enviar
fmt.Println(msg) // imprime "olá"
Útil para handoff: produtor entrega item ao consumidor, ambos sincronizam no momento da entrega. Para pipelines onde produtor e consumidor têm taxas diferentes, channel buffered costuma ser melhor.
Buffered channels — assíncronos com limite
ch := make(chan int, 10) cria channel com buffer
de 10. Send só bloqueia quando o buffer está cheio; receive só
bloqueia quando está vazio. É a primitiva de backpressure mais
direta em Go: o produtor é forçado a esperar quando o consumidor
está atrasado, evitando crescimento de fila ilimitado. O
conceito 12 (backpressure) volta nesse ponto.
Direção e fechamento
Channels podem ser tipados como send-only (chan<- T)
ou receive-only (<-chan T) em assinaturas de
função. Isso documenta intenção e permite o compilador detectar
mau uso. close(ch) sinaliza que ninguém mais vai
enviar; receives subsequentes em canal fechado retornam zero
value imediatamente. v, ok := <-ch distingue valor
recebido de canal fechado (ok vira false).
for v := range ch itera consumindo até o canal
fechar — padrão idiomático para consumir um stream.
func produzir(out chan<- int) {
defer close(out) // sinaliza "acabou" no fim
for i := 0; i < 5; i++ {
out <- i
}
}
func main() {
ch := make(chan int)
go produzir(ch)
for v := range ch { // consome até close
fmt.Println(v)
}
}
// 0 1 2 3 4
select — multiplexar canais
O statement select é o multiplexador de Go: aguarda
simultaneamente várias operações de channel e procede com a
primeira que ficar pronta. É a primitiva que diferencia CSP
idiomático de simples filas — ela permite expressar
"estou esperando por qualquer destas coisas".
select {
case v := <-ch1:
fmt.Println("recebido de ch1:", v)
case ch2 <- "olá":
fmt.Println("enviado em ch2")
case <-time.After(5 * time.Second):
fmt.Println("timeout")
default:
fmt.Println("nenhum pronto agora")
}
Quatro cases mostram os padrões. case v := <-ch1
espera receber de ch1. case ch2 <- "olá" espera
enviar em ch2. time.After retorna um canal que
recebe um valor após N tempo — padrão idiomático de timeout.
default torna o select non-blocking (executa
imediatamente se nenhum outro estiver pronto).
Quando múltiplos cases estão prontos simultaneamente,
select escolhe um pseudo-aleatoriamente, evitando
starvation. É um detalhe sutil mas importante: você não pode
depender da ordem dos cases para priorizar.
select é a primitiva onde Go expressa a maior
parte do que outras linguagens fazem com mutex, condition
variables ou Promise.race. Uma vez que você internalizou o
padrão, a maior parte da concorrência em Go pode ser
expressada como composição de canais e selects.
Padrões idiomáticos com goroutines e channels
A tradição de Go consolidou um pequeno conjunto de padrões que cobrem a maior parte do uso prático. Vale conhecer pelo nome — o conceito 11 (padrões de produção) volta neles com mais profundidade.
Worker pool
N goroutines consumindo de um channel de jobs comum, escrevendo em um channel de resultados. Limita concorrência total (importante para não exhaurir banco, API externa, ou memória).
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ { // pool de 3 workers
go worker(w, jobs, results)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
for r := 1; r <= 5; r++ {
fmt.Println(<-results)
}
}
Fan-out / fan-in
Um produtor; várias goroutines consumindo (fan-out); um único consumidor agregando (fan-in). Útil quando o trabalho por item é caro mas pode ser feito em paralelo.
Pipeline
Estágios encadeados, cada um lendo de um channel e escrevendo no próximo. A primeira recebe input externo; a última escreve output externo. Excelente para processamento de stream com transformações sucessivas.
// gerador → estágio 1 → estágio 2 → consumidor
nums := gerador()
quadrados := elevar(nums)
soma := somar(quadrados)
fmt.Println(soma)
Done channel — cancelamento sinalizado
Antes de context.Context (introduzido em Go 1.7,
2016), o padrão idiomático era passar um channel
done chan struct{} que, quando fechado, sinaliza
"pode parar". Goroutines monitoram via select com
case <-done. Hoje context assumiu
esse papel, mas o padrão done channel ainda aparece em código
de bibliotecas que querem evitar dependência de
context.
context.Context — cancelamento idiomático
context.Context, introduzido por Sameer Ajmani em
2014 originalmente como parte do Google Go ecosystem e
promovido para a stdlib em Go 1.7 (2016), é a forma idiomática
de cancelamento, deadline e propagação de valores em chamadas
concorrentes em Go. Conceito 13 vai aprofundar; aqui basta
mostrar a integração natural com goroutines e channels.
func tarefaComCancelamento(ctx context.Context, ch chan<- int) {
for i := 0; ; i++ {
select {
case <-ctx.Done(): // cancelamento sinalizado
fmt.Println("cancelado:", ctx.Err())
return
case ch <- i:
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
ch := make(chan int)
go tarefaComCancelamento(ctx, ch)
for v := range ch {
fmt.Println("recebido:", v)
}
}
// imprime alguns valores, depois "cancelado: context deadline exceeded"
A convenção forte é: aceite ctx como primeiro parâmetro em qualquer função que pode demorar ou ser cancelada. Isso vale para HTTP requests, queries de banco, RPCs, e funções próprias com loops longos. O contexto se propaga por toda a call stack, e cancelamento funciona end-to-end. É a forma idiomática que Go adotou para o que outras linguagens fazem com structured concurrency formal.
CSP vs Actor model — uma nota comparativa
No próximo conceito (07) mergulhamos em actor model. Vale adiantar a comparação porque a confusão é frequente: os dois modelos parecem similares — ambos baseiam concorrência em mensagens — mas têm diferenças semânticas importantes.
Em CSP, o canal é o cidadão de primeira classe. Você cria canais, passa canais como parâmetros, fecha canais. Os processos comunicantes são anônimos do ponto de vista do canal — qualquer goroutine que tenha referência ao canal pode enviar ou receber. Comunicação é tipicamente síncrona (canais unbuffered de Go encarnam isso).
No actor model de Carl Hewitt (1973), o actor é o cidadão de primeira classe. Cada actor tem identidade (address) e mailbox próprio. Você envia mensagem para um actor específico (não para um canal compartilhado), e a mensagem fica no mailbox dele até ser processada. Comunicação é tipicamente assíncrona (envio nunca bloqueia).
Erlang, Akka, Orleans implementam actor model. Go implementa CSP. Você pode simular um modelo no outro com algum esforço: uma goroutine + channel funciona como actor (a goroutine lê mensagens do channel = mailbox); um actor pode simular CSP definindo um actor "channel" que reenvia. Mas a ergonomia da linguagem favorece o modelo nativo. Os dois são adequados em situações distintas — actor model brilha em sistemas distribuídos com identidades de longa vida; CSP brilha em pipelines de processamento e fluxos coordenados.
O mesmo problema nas três linguagens
Para concretizar como CSP se traduz fora de Go, considere o padrão de pipeline simples: gerar números, dobrar cada um, imprimir os resultados. Em Go, é canônico; em C# e Python, requer bibliotecas que tragam channels para um ambiente que historicamente não os tinha.
using System.Threading.Channels;
using System.Threading.Tasks;
async Task RodarPipeline() {
var canal1 = Channel.CreateBounded<int>(10);
var canal2 = Channel.CreateBounded<int>(10);
// Gerador
_ = Task.Run(async () => {
for (int i = 0; i < 5; i++)
await canal1.Writer.WriteAsync(i);
canal1.Writer.Complete();
});
// Estágio 2: dobra
_ = Task.Run(async () => {
await foreach (var v in canal1.Reader.ReadAllAsync())
await canal2.Writer.WriteAsync(v * 2);
canal2.Writer.Complete();
});
// Consumidor
await foreach (var r in canal2.Reader.ReadAllAsync())
Console.WriteLine(r);
}
System.Threading.Channels, introduzido em
.NET Core 3.0 (2019), é uma implementação CSP-like
inspirada diretamente em Go. A API é mais verbosa (escrita,
leitura, complete são métodos separados), mas a semântica
é idêntica. Bounded oferece backpressure
natural — o writer aguarda quando o buffer enche.
import asyncio
async def gerador(out: asyncio.Queue):
for i in range(5):
await out.put(i)
await out.put(None) # sinaliza "acabou"
async def dobrar(in_q: asyncio.Queue, out: asyncio.Queue):
while (v := await in_q.get()) is not None:
await out.put(v * 2)
await out.put(None)
async def consumir(in_q: asyncio.Queue):
while (v := await in_q.get()) is not None:
print(v)
async def main():
q1 = asyncio.Queue(maxsize=10)
q2 = asyncio.Queue(maxsize=10)
async with asyncio.TaskGroup() as tg:
tg.create_task(gerador(q1))
tg.create_task(dobrar(q1, q2))
tg.create_task(consumir(q2))
asyncio.run(main())
asyncio.Queue é o equivalente mais próximo de
channel em Python. Com maxsize dá backpressure.
Não tem fechamento built-in — você precisa de sentinela
(None aqui). Trio tem MemoryChannel com API
mais próxima de CSP. Combinado com TaskGroup,
o padrão fica estruturado.
package main
import "fmt"
func gerador(out chan<- int) {
defer close(out)
for i := 0; i < 5; i++ {
out <- i
}
}
func dobrar(in <-chan int, out chan<- int) {
defer close(out)
for v := range in {
out <- v * 2
}
}
func main() {
c1 := make(chan int, 10)
c2 := make(chan int, 10)
go gerador(c1)
go dobrar(c1, c2)
for v := range c2 {
fmt.Println(v)
}
}
Em Go o padrão é nativo. Channels têm close que
sinaliza fim sem sentinela. range sobre channel
itera até close. Direção (chan<-,
<-chan) documenta intenção e ajuda o
compilador. Note como cada estágio é uma função simples,
composta por canais — a estrutura léxica espelha a
estrutura concorrente.
Armadilhas em CSP — deadlock e leaks
CSP é mais simples que threads tradicionais em vários eixos, mas tem suas próprias armadilhas. As mais comuns:
Deadlock por send sem receive
Channel unbuffered: send bloqueia até receive. Se a única goroutine que receberia bloqueia esperando outra coisa que depende deste send, deadlock. Em Go, o runtime detecta deadlock em programa todo (todas goroutines bloqueadas) e crasha com mensagem clara — bom para desenvolvimento, mas raramente acontece em produção real onde há tarefas infinitas (servidores HTTP) que mascaram o caso.
Goroutine leak
Goroutine bloqueada para sempre em receive de canal que ninguém
vai escrever, ou em send que ninguém vai consumir. Não crasha o
programa, mas a goroutine fica viva consumindo memória até o
processo terminar. Em servidores long-lived, isso vaza
memória progressivamente. Detectar: pprof (com
flag ?debug=2) mostra todas as goroutines vivas.
Prevenir: cancelamento via context em todo loop
que aguarda channel, garantindo saída em qualquer caminho.
Range sobre canal não fechado
for v := range ch só termina quando ch
é fechado. Se ninguém fecha, o range fica para sempre. Padrão
seguro: o produtor é responsável pelo close (com
defer close(ch) idealmente). Múltiplos produtores
sobre mesmo channel é regra: precisa de coordenação extra
(errgroup, sync.WaitGroup) para decidir quem fecha — fechamento
duplo causa panic.
Compartilhar channel entre múltiplos produtores e tentar
fechá-lo do produtor causa pânico se outro já fechou. Padrão
idiomático: o produtor único fecha. Para múltiplos produtores,
use sync.WaitGroup (wg.Wait()) numa
goroutine "fechadora" que aguarda todos terminarem antes de
chamar close(ch).
Quando CSP brilha — e quando outros modelos vencem
CSP é especialmente bom quando a estrutura do problema é fluxo: dados entrando, sendo transformados em estágios, saindo. Pipelines de processamento, ETL, scrapers, streaming, sistemas de mensageria — onde fluxo é a metáfora natural, channels e selects expressam diretamente o que você quer.
CSP é menos natural em sistemas com identidade de longa vida: cada usuário tem estado próprio, cada conexão tem contexto, atores precisam manter estado entre mensagens. Para esses casos, actor model (próximo conceito) costuma ser melhor modelagem. Você pode implementar identidade em Go com goroutines de longa vida que serializam acesso ao estado, mas o código fica mais verboso que o equivalente em Erlang/Akka.
Memória compartilhada com mutex (próximos conceitos 08–09) ainda é apropriada quando o estado é pequeno e altamente contendido — um contador, um cache, uma estrutura de dados compartilhada. Forçar tudo a passar por channel adiciona overhead sem ganho. A regra de Pike é defensiva: prefira channel para fluxo; use mutex para sincronizar acesso a estado simples.
Como praticar
-
Implemente um pipeline com três estágios. Em
Go, gere números aleatórios, filtre os pares, eleve ao
quadrado, imprima. Cada estágio é uma goroutine; channels os
ligam. Use
defer closecorretamente em cada produtor. Confirme quemainespera o consumidor terminar antes de sair. -
Construa um worker pool com cancelamento. N
workers consumindo de um channel de jobs; cada worker honra
um
context.Contexte termina graciosamente quando cancelado. Useselectdentro de cada worker para escolher entre "consumir job" e "fim do contexto". Force timeout viacontext.WithTimeoute observe os workers encerrando. -
Compare channel vs mutex. Implemente um
contador concorrente de duas formas: (a) goroutine que
escuta um channel, incrementando para cada mensagem, devolve
valor por outro channel; (b)
sync.Mutexprotegendo umint. Faça benchmark comtesting.Ba 100k operações. Compare. Pense em que cenários cada um faz mais sentido.
Referências para aprofundar
- livro The Go Programming Language — Alan A. A. Donovan & Brian W. Kernighan (2015).
- livro Concurrency in Go — Katherine Cox-Buday (2017).
- livro Communicating Sequential Processes — C. A. R. Hoare (1985, gratuito online).
- livro 100 Go Mistakes and How to Avoid Them — Teiva Harsanyi (2022).
- paper Communicating Sequential Processes — C. A. R. Hoare (CACM, 1978).
- artigo Go's work-stealing scheduler design — Dmitry Vyukov (2012).
- artigo Go Concurrency Patterns: Context — Sameer Ajmani (2014).
- artigo Go Concurrency Patterns: Pipelines and cancellation — Sameer Ajmani (2014).
- docs Effective Go — Concurrency.
- docs The Go Memory Model.
- vídeo Go Concurrency Patterns — Rob Pike (Google I/O, 2012).
- vídeo Advanced Go Concurrency Patterns — Sameer Ajmani (Google I/O, 2013).