No gerador uma gorrotina escrevia no canal. No trabalhador ela lia.
E se ela fizer os dois? Lê de um canal, transforma o valor e escreve em outro.
O que é um pipeline Link para o cabeçalho
Um pipeline é uma cadeia de estágios ligados por canais. Cada estágio (stage) é uma função que recebe um canal, dispara uma gorrotina para ler dele e devolve o canal onde escreve o resultado.
Ou seja, cada estágio é trabalhador de quem vem antes e gerador para quem vem depois.
Vamos começar com um estágio que dobra o que recebe:
func dobro(entrada <-chan int) <-chan int {
saida := make(chan int)
go func() {
for valor := range entrada {
saida <- valor * 2
}
// quando a entrada fecha, a saída fecha também
close(saida)
}()
return saida
}
Repare na assinatura. A dobro recebe um <-chan int e devolve outro <-chan int. Como a saída tem o mesmo tipo da entrada, você pode encaixar um dobro dentro do outro:
func main() {
for valor := range dobro(dobro(sequenciaNumeros(1, 10))) {
fmt.Printf("valor: %v\n", valor)
}
}
A sequenciaNumeros é a mesma do post dos geradores, por enquanto sem contexto. Ela gera de 1 a 10, o primeiro dobro multiplica por dois e o segundo multiplica de novo:
$ go run .
valor: 4
valor: 8
...
valor: 40
São três gorrotinas trabalhando ao mesmo tempo, e cada canal carrega um valor por vez.
Mas quem fechou o canal da main? Link para o cabeçalho
Repare que a main não fechou canal nenhum, e mesmo assim o range dela terminou.
Quem fechou foi o gerador, depois de mandar o 10. O range do primeiro dobro terminou, ele fechou a própria saída, o range do segundo terminou, fechou a dele, e o range da main acabou.
Logo, um close na ponta de cima encerra a cadeia inteira, e cada estágio só precisa fechar o canal que ele mesmo criou.
E se eu parar de ler no meio? Link para o cabeçalho
E se, como no gerador, eu der um break no meio?
Parei logo depois do primeiro valor e contei quem ficou vivo:
for valor := range dobro(dobro(sequenciaNumeros(1, 10))) {
if valor == 8 {
break
}
fmt.Printf("valor: %v\n", valor)
}
fmt.Println("gorrotinas vivas:", runtime.NumGoroutine())
// valor: 4
// gorrotinas vivas: 4
Opa! A main é uma. As outras três são o gerador e os dois estágios, cada um parado em um envio que ninguém vai ler.
No gerador ficava uma gorrotina para trás. Aqui fica uma por estágio, e quanto mais comprido o pipeline, mais gorrotinas sobram.
A solução Link para o cabeçalho
A solução é a mesma do gerador, um context.Context. A diferença é que ele precisa atravessar a cadeia inteira:
-func dobro(entrada <-chan int) <-chan int {
+func dobro(ctx context.Context, entrada <-chan int) <-chan int {
saida := make(chan int)
go func() {
+ defer close(saida)
for valor := range entrada {
- saida <- valor * 2
+ select {
+ case saida <- valor * 2:
+ case <-ctx.Done():
+ return
+ }
}
- // quando a entrada fecha, a saída fecha também
- close(saida)
}()
return saida
}
A sequenciaNumeros volta a ser a versão com contexto do post dos geradores, e a main passa o mesmo ctx para os três:
ctx, cancelar := context.WithCancel(context.Background())
valores := dobro(ctx, dobro(ctx, sequenciaNumeros(ctx, 1, 10)))
for valor := range valores {
if valor == 8 {
break
}
fmt.Printf("valor: %v\n", valor)
}
cancelar()
// drena até o último estágio fechar
for range valores {
}
fmt.Println("pipeline encerrado")
// valor: 4
// pipeline encerrado
Um único cancelar() e as três gorrotinas vão embora. Cada uma sai pelo ctx.Done(), sem depender de o estágio seguinte voltar a ler, e fecha o canal que criou no defer.
Na verdade, como a main continua drenando, às vezes o select escolhe o envio, e o estágio acaba saindo porque a entrada dele fechou. O fim é o mesmo.
É o que o artigo Go Concurrency Patterns: Pipelines and cancellation, do Sameer Ajmani, chama de cancelamento explícito.
E quando um estágio dá erro? Link para o cabeçalho
O exemplo não trata erro, e não existe um jeito só de fazer isso. O estágio pode devolver um segundo canal só de erros e parar no primeiro, ou mandar o erro junto com o valor em uma struct e deixar quem lê decidir. Também dá para o estágio receber um canal de erros e seguir em frente.
Acho que depende de um erro dever ou não parar os outros estágios, e isto volta mais para frente na série, quando chegarmos ao semáforo.
De onde veio o exemplo Link para o cabeçalho
O código está na pasta pipeline do concorrencia-go, com o teste que confere os dez valores multiplicados por quatro.
No próximo vem o fan-out. Quando um estágio só não dá conta, vários trabalhadores leem do mesmo canal.
Lembrando que se o ctx entra na cadeia, ele entra em todos os estágios. Basta um ficar de fora para aquele estágio ficar preso no envio.
Então é isso, pessoal!
Até a próxima!
{}’s