No trabalhador o fmt.Printf fazia o papel do processamento. Agora vamos fingir que ele demora.

Um time.Sleep de 100 milissegundos por valor. São dez valores, ou seja, um segundo parado olhando para a tela.

E se eu colocar três trabalhadores lendo do mesmo canal?

O que é um fan-out Link para o cabeçalho

Um fan-out distribui os valores de um canal entre várias gorrotinas. Cada valor vai para uma só delas, e com isso um trabalho demorado se divide entre quem estiver livre.

Ele também aparece como distribuição de trabalho ou work distribution. O artigo Go Concurrency Patterns: Pipelines and cancellation define assim: múltiplas funções lendo do mesmo canal até ele ser fechado.

O trabalhador ganhou um id para a gente saber quem pegou o quê:

func trabalhador(id int, entrada <-chan int) {
	for valor := range entrada {
		fmt.Printf("id: %d processando valor: %v\n", id, valor)
		// simula um processamento demorado
		time.Sleep(100 * time.Millisecond)
	}
}

func fanout(entrada <-chan int, n int) {
	var wg sync.WaitGroup
	for i := range n {
		wg.Go(func() {
			trabalhador(i+1, entrada)
		})
	}
	// só retorna quando os n trabalhadores terminarem
	wg.Wait()
}

A sequenciaNumeros é a mesma dos geradores, e a main manda os dez valores para três trabalhadores:

func main() {
	ctx, cancelar := context.WithCancel(context.Background())
	defer cancelar()

	fanout(sequenciaNumeros(ctx, 1, 10), 3)
}
$ go run .
id: 3 processando valor: 1
id: 1 processando valor: 2
id: 2 processando valor: 3
id: 1 processando valor: 5
id: 2 processando valor: 4
id: 3 processando valor: 6
id: 2 processando valor: 7
id: 3 processando valor: 8
id: 1 processando valor: 9
id: 2 processando valor: 10

Preste atenção nos valores. Nenhum aparece duas vezes, e nenhum ficou de fora.

Rodei aqui com um trabalhador e com três. Um segundo contra 400 milissegundos.

Mas quem decide qual valor vai para qual trabalhador? Link para o cabeçalho

Ninguém. Repare que não existe uma linha de código escolhendo o trabalhador, nem um índice, nem um sorteio.

Quando várias gorrotinas estão paradas lendo o mesmo canal, cada envio é entregue a uma só delas. Quem estiver livre primeiro, recebe. O canal já faz a distribuição, e o fanout só precisa disparar as gorrotinas.

Mas o que é esse WaitGroup? Link para o cabeçalho

No post do trabalhador a main esperava um <-pronto para saber que ele tinha acabado. Com três trabalhadores seriam três canais, ou três leituras do mesmo. E se forem cinquenta?

O sync.WaitGroup é um contador. O wg.Go dispara a gorrotina e soma um, e quando ela retorna ele subtrai. O wg.Wait() fica parado até o contador chegar a zero.

Hummm 🤔 Mas a série inteira não era sobre canais?

Era, e continua sendo. Os canais estão levando os dados, que é o trabalho deles. Contar quantas gorrotinas já terminaram é um problema menor, e para esses o Rob Pike recomenda o pacote sync. Na palestra Go Concurrency Patterns ele avisa “Don’t overdo it”, porque às vezes só é preciso um contador.

Daqui para frente na série os canais transportam os dados e o WaitGroup só conta quem terminou.

Rode mais uma vez Link para o cabeçalho

Rodei de novo e a saída mudou:

$ go run .
id: 3 processando valor: 2
id: 2 processando valor: 3
id: 1 processando valor: 1
id: 1 processando valor: 4
...

Os três trabalhadores concorrem pelos valores, e quem decide qual deles roda em cada momento é o escalonador do Go, não você.

É a primeira vez na série que a ordem dos valores muda de uma execução para outra. O Rob Pike explica isto na palestra Concurrency is not parallelism. O programa descreve computações independentes, mas não diz em que ordem elas executam. Logo, se o seu programa só funciona com uma ordem específica, ele está errado, mesmo que passe no teste hoje.

De onde veio o exemplo Link para o cabeçalho

O código está na pasta fan_out do concorrencia-go. O teste de lá usa um trabalhador só, justamente porque com três a ordem muda a cada execução.

No próximo vem o tee, que é fácil de confundir com este. No fan-out cada valor vai para um trabalhador. No tee cada valor é copiado para todos.

Lembre-se que o wg.Go só existe a partir do Go 1.25. Antes dele era wg.Add(1) antes do go e defer wg.Done() dentro da gorrotina.

Então é isso, pessoal!

Até a próxima!

{}’s