Padrões de concorrência
Podemos organizar a comunicação entre goroutines usando diferentes padrões de concorrência, dois padrões comuns são fan-in (convergência) e fan-out (divergência), que definem como as tarefas são distribuídas e como seus resultados são recebidos. Esses padrões não são recursos específicos da linguagem, mas formas de organizar goroutines e channels para trabalhar com várias operações concorrentes.
Convergência (fan-in)
A convergência (fan-in) acontece quando várias goroutines enviam seus resultados para um único channel, permitindo que outra parte do programa receba essas informações em um mesmo lugar. Isso é útil quando temos várias tarefas independentes, como consultar usuários e produtos, mas queremos reunir os resultados para processá-los.
Goroutine 1 ──────┐
│
v
Channel ──────> main
^
│
Goroutine 2 ──────┘
package main
import "fmt"
func main() {
// Cria um channel para receber os resultados.
resultados := make(chan string, 2)
// Primeira goroutine.
go func() {
resultados <- "Usuários encontrados"
}()
// Segunda goroutine.
go func() {
resultados <- "Produtos encontrados"
}()
// Recebe os resultados das duas goroutines.
fmt.Println(<-resultados)
fmt.Println(<-resultados)
}
Resultado possível:
Produtos encontrados
Usuários encontrados
As duas goroutines enviam informações para o mesmo channel resultados. A função main() recebe esses valores conforme ficam disponíveis, sem precisar acessar cada goroutine separadamente. A ordem dos resultados não é garantida, porque depende de qual goroutine consegue enviar primeiro.
Divergência (fan-out)
A divergência (fan-out) acontece quando distribuímos tarefas entre várias goroutines, permitindo que elas trabalhem de forma concorrente. Isso é útil quando temos muitas tarefas para processar, como arquivos, mensagens ou requisições, e queremos dividir o trabalho entre diferentes goroutines.
┌──────> Goroutine 1
│
Tarefas ──> Channel ┤
│
└──────> Goroutine 2
package main
import (
"fmt"
"sync"
)
func trabalhador(id int, tarefas <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
// Recebe tarefas até o channel ser fechado.
for tarefa := range tarefas {
fmt.Printf("Trabalhador %d processou tarefa %d\n", id, tarefa)
}
}
func main() {
// channel sem buffer:
tarefas := make(chan int)
var wg sync.WaitGroup
wg.Add(2)
// Cria duas goroutines que recebem do mesmo channel.
go trabalhador(1, tarefas, &wg)
go trabalhador(2, tarefas, &wg)
// Envia quatro tarefas para processamento.
for i := 1; i <= 4; i++ {
tarefas <- i
}
// Informa que não haverá novas tarefas.
close(tarefas)
// Aguarda os trabalhadores terminarem.
wg.Wait()
}
Resultado possível:
Trabalhador 2 processou tarefa 1
Trabalhador 1 processou tarefa 2
Trabalhador 2 processou tarefa 3
Trabalhador 1 processou tarefa 4
Aqui criamos duas goroutines que executam a mesma função trabalhador(), recebendo tarefas de um único channel. Quando enviamos um número para tarefas, uma das goroutines disponíveis recebe esse valor e faz o processamento. Cada valor enviado é recebido por apenas uma goroutine, ou seja, o channel não envia uma cópia da mesma tarefa para todos os trabalhadores.
Depois de enviar as quatro tarefas, fechamos o channel com close(tarefas), permitindo que os trabalhadores encerrem seus respectivos for range quando não houver mais valores. O WaitGroup garante que a função main() aguarde a conclusão das duas goroutines.
Diferença entre fan-in e fan-out
A principal diferença está na direção em que os dados são distribuídos. No fan-in, várias goroutines produzem informações que são reunidas em um único channel. Já no fan-out, várias goroutines recebem tarefas de uma fonte compartilhada, dividindo o processamento entre elas.
Também podemos combinar os dois padrões, distribuindo tarefas entre várias goroutines com fan-out e depois reunindo os resultados em outro channel com fan-in. Essa combinação é comum em programas que precisam processar muitas informações de forma concorrente, principalmente quando queremos controlar quantas goroutines trabalham ao mesmo tempo.