No fan-out três trabalhadores dividiam um canal, mas o resultado morria em um fmt.Printf. No fan-in várias gorrotinas escreviam no mesmo canal de saída.
E se eu juntar os dois? Vários trabalhadores lendo da mesma entrada e escrevendo o resultado na mesma saída.
O que é um grupo de trabalhadores Link para o cabeçalho
Um grupo de trabalhadores é um número fixo de gorrotinas que esperam valores em um canal de entrada. Cada uma processa o que pegou, escreve o resultado em um canal de saída e volta para a fila.
Ele também aparece como worker pool ou pool de gorrotinas. Aqui em casa minha esposa chama de piscina de marmotinhas, e confesso que o nome pegou.
O trabalhador é o do fan-out, mas agora com uma saída:
func trabalhador(ctx context.Context, id int, entrada <-chan int, saida chan<- int) {
for valor := range entrada {
fmt.Printf("id: %d processou valor: %v\n", id, valor)
select {
case saida <- valor * 2:
case <-ctx.Done():
return
}
}
fmt.Printf("id: %d terminou\n", id)
}
E o grupo dispara n deles e devolve a saída:
func grupoDeTrabalhadores(ctx context.Context, entrada <-chan int, n int) <-chan int {
saida := make(chan int)
var wg sync.WaitGroup
for i := range n {
wg.Go(func() {
trabalhador(ctx, i+1, entrada, saida)
})
}
// quando todos terminarem, fecha a saída
go func() {
wg.Wait()
close(saida)
}()
return saida
}
A sequenciaNumeros é a dos geradores, e a main manda dez valores para dois trabalhadores:
func main() {
ctx, cancelar := context.WithCancel(context.Background())
defer cancelar()
saida := grupoDeTrabalhadores(ctx, sequenciaNumeros(ctx, 1, 10), 2)
for resultado := range saida {
fmt.Println(resultado)
}
}
$ go run .
id: 1 processou valor: 1
id: 1 processou valor: 3
2
6
id: 2 processou valor: 2
4
...
id: 2 terminou
id: 1 terminou
Os dez resultados aparecem, dobrados e fora de ordem, como no fan-out.
Repare que o trabalhador nem sabe que o WaitGroup existe. Ele lê, processa e escreve. Quem o disparou é que conta quem terminou, e é a mesma gorrotina do fan-in que espera no wg.Wait() para fechar a saída.
Mas isto não é o fan-out? Link para o cabeçalho
Na entrada, é. Várias gorrotinas lendo do mesmo canal, e o canal entregando cada valor a uma só.
A diferença está na saída. O fan-out só distribuía o trabalho, e o grupo também junta os resultados em um canal só, que é exatamente o que o fan-in fazia.
Logo, o grupo de trabalhadores é um fan-out na entrada e um fan-in na saída. Nada novo, os dois padrões emendados.
Mas por que o select no envio? Link para o cabeçalho
Dá vontade de escrever só saida <- valor * 2 no trabalhador. Eu escrevi, e depois parei de ler a saída no meio, como já fiz no pipeline:
for resultado := range saida {
fmt.Println(resultado)
break
}
cancelar()
Rodei 200 vezes aqui e em 170 ficaram quatro gorrotinas vivas: a main, os dois trabalhadores e a que espera no wg.Wait().
O cancelar() encerra o gerador, mas cada trabalhador já pegou um valor e está parado no saida <- valor * 2, esperando alguém ler. Ninguém vai ler. O WaitGroup nunca chega a zero, a saída nunca fecha, e a gorrotina do wg.Wait() fica presa junto.
Com o select, o ctx.Done() tira os trabalhadores do envio, o contador zera e a saída é fechada do mesmo jeito. Nas 200 execuções sobrou só a main.
É a pulga atrás da orelha do olá mundo, o vazamento de gorrotinas. Com um trabalhador só era uma gorrotina presa. Aqui é o grupo inteiro.
De onde veio o exemplo Link para o cabeçalho
O código está na pasta grupo do concorrencia-go. O teste de lá usa um trabalhador só, pelo mesmo motivo do fan-out.
No próximo vem a contrapressão, que apareceu de relance no tee, quando todo mundo andava no passo do trabalhador mais lento.
Lembre-se que quantos trabalhadores colocar no grupo depende do que eles fazem. Para contas pesadas, mais trabalhadores que núcleos não ajuda. Para esperar rede ou disco, pode ser bem mais, porque a maior parte do tempo eles ficam parados.
Então é isso, pessoal!
Até a próxima!
{}’s