Programador Go 4-chan
(dolthub.com)- A DoltHub criou um exemplo de brincadeira ao aninhar deliberadamente em excesso canais, comuns em concorrência em Go, enviando canais por canais
- Em um código herdado de fato havia
chan chan struct{}, usado em um padrão de fan-out que entregava um novo canal a uma goroutine worker, mas ele foi reescrito por ser difícil de raciocinar e gerenciar - O exemplo amplia para o
chando Go a piada do “4-star programmer” comint****da família C, usando_4chan:= make(chan chan chan chan int)como canal de nível superior - Com
factor = 3, ele ramifica produtores e consumidores em cada camada de canais e soma os valores finaisint, imprimindo 243, ou seja, 3 elevado à 5ª potência - Na prática, é inadequado por causa da dificuldade de implementação e depuração, do tratamento de fechamento de canais, da necessidade de
sync.WaitGroupe de vazamentos de goroutines; o exemplo depende detime.Sleep()em vez de lógica de encerramento
Caso de aninhamento de canais vindo do Dolt
- A DoltHub está escrevendo em Go o Dolt, o primeiro banco de dados SQL com controle de versão do mundo
- Como em bases de código Go comuns, usa channels e goroutines para implementar execução concorrente
- Como programação concorrente já é difícil por si só, normalmente canais e goroutines são tratados de forma simples e intuitiva
- Em certo momento, um código trazido de outro projeto open source tinha um canal que enviava canais, como abaixo
var c chan chan struct{}
- Essa estrutura era uma forma de implementar um padrão de fan-out de goroutines worker, passando canais entre goroutines
- O canal intermediário atuava como um mediador que entregava um canal recém-criado ao worker que fazia o trabalho real
- Funcionava, mas era difícil de raciocinar e lidar, especialmente considerando até vazamentos de goroutines
- O código em questão foi reescrito, e
chan chan struct{}desapareceu
Versão em Go da piada do “4-star programmer”
- Na época em que C e linguagens derivadas eram amplamente usadas, havia uma piada sobre iniciantes com dificuldade para entender ponteiros: o “4-star programmer”
- O exemplo típico é um código que usa várias etapas de indireção de ponteiros, como
int**** - Como Go também deriva em grande parte de C, é possível escrever o mesmo tipo de código com ponteiros
- Passando
*int,**int,***inte****intem sequência - Se a última função executar
****i = 100, o programa imprimei is now 100
- Passando
- Como Go tem
chan, que C não tem, a mesma piada pode ser ampliada para indireção por canais
Calculando a 5ª potência com canais em 4 níveis
- O canal de nível superior é declarado assim
_4chan := make(chan chan chan chan int)
- Identificadores em Go não podem começar com números, então o exemplo usa o nome
_4chan - O valor enviado por
_4chané um canal de 3 níveis_3chan := make(chan chan chan int)
- Da mesma forma, ele desce pelas camadas até chegar, no fim, a um canal de valores
chan int - Em cada camada de indireção, produtores são criados de acordo com a constante
factor- No exemplo,
const factor = 3 sendChanChanChaninicia um produtor de canais de 3 níveis como goroutine
- No exemplo,
- No lado do consumidor, em cada camada ele recebe o canal recebido e inicia consumidores da próxima etapa
factorvezesreceiveChanChanChanrecebe_3chande_4chane inicia consumidores de canais de 3 níveis
Envio e soma de valores na última camada
- Na camada mais baixa, não se enviam mais canais, mas sim valores
intreais - A função
sendenvia_1chanpara_2chane depois inicia produtores de intfactorvezes - Cada produtor de int cria novamente
factorgoroutines para executar_1chan <- 1 - O consumidor soma o inteiro recebido à variável global
sumsumé declarada comoatomic.Int32receive(c chan int)recebe um valor do canal e executasum.Add(int32(s))
Resultado da execução e número de ramificações
- O programa completo cria
_4chan, inicia as camadas de envio e recebimento, cada uma em uma goroutine, e então espera por500 * time.Millisecond - A saída do exemplo é a seguinte
3 ^ 5: 243
- Esse programa é um exemplo generalizado que calcula a 5ª potência de um número da forma mais distribuída possível
- O exemplo executável pode ser visto no Go Playground, e a versão com realce de sintaxe está no GitHub Gist
- Para usar um
factormaior, talvez seja necessário aumentar o tempo deSleeppara que a execução consiga terminar - Ao ativar os logs, é possível conferir o número de ramificações de cada camada de produção e consumo de canais
starting 3chan producer: 3 vezesstarting 2chan producer: 9 vezesstarting 3chan consumer: 9 vezesstarting 2chan consumer: 27 vezesstarting chan producer: 27 vezesstarting 1chan consumer: 81 vezesstarting int producer: 81 vezessending int: 243 vezesreceived int: 243 vezes
Por que evitar em código real
- Essa abordagem é complicada de implementar e depurar em código real
- Quando canais são enviados por canais, fica difícil decidir quando fechar cada canal
- Em um caso de uso real, seria preciso fechar os canais, mas para adicionar a lógica de encerramento seria necessário rastrear se todos os envios por canais terminaram
- Para implementar o encerramento, seria necessário adicionar
sync.WaitGroupem vários pontos, o que tornaria o exemplo de brincadeira difícil de ler - O exemplo final foi simplificado para usar
time.Sleep()em vez de lógica de encerramento, deixando muitos vazamentos de goroutines
1 comentários
Opiniões no Hacker News
Do ponto de vista de um cientista que trabalha de perto com engenheiros de software profissionais de verdade, muito do que eles fazem parece ser desse tipo, e é muito difícil entender por que fazem assim.
Já vi uma única linha de código passar, em sequência, por 4 funções de interface antes de ser realmente chamada, com essas funções espalhadas por arquivos diferentes em pastas diferentes.
Por isso, tentar ler o que o código faz fica cansativo; depois de entrar alguns níveis, você começa a duvidar se está olhando para o lugar certo e se algum dia vai chegar ao ponto onde o cálculo real acontece.
A sensação de que parece excessivo e confuso não está errada; quando você escreve código “interessante” pela primeira vez, ele pode parecer tecnicamente complexo e até elegante, mas dentro de um software que precisa crescer de verdade isso vira um pesadelo técnico.
Passei quase 2 anos limpando uso indevido de channels em código Go, e o problema é que channels raramente são de fato necessários, mas no começo parecem fáceis de usar para várias finalidades.
O critério para saber se você está usando channels corretamente é conseguir responder “não” a “não dá para fazer com uma chamada direta de função?” e “não dá para fazer com wait group ou mutex?”, e “sim” a “o ganho de concorrência/paralelismo é grande o bastante para justificar a complexidade de depurar código concorrente?”.
Às vezes há uma falta assustadora de compreensão e competência onde elas deveriam existir.
Na graduação, fiz pair programming por uns 30 minutos com um doutor em Ciência da Computação, e foi bastante esclarecedor.
Ele não entendia nada de software, a ponto de apontar que não estávamos verificando se o tamanho de uma estrutura de dados da biblioteca padrão era negativo.
Dito isso, às vezes há motivos para esse tipo de estrutura. Às vezes ela é de fato razoável; outras vezes, é uma forma de lidar com uma base de código insana deixada por quem veio antes.
Já li código usado em artigos de pesquisa, e, como a matemática teórica em geral está além da minha compreensão, quando entrava no código para tentar ver a lógica, muitas vezes era ainda pior e mais indecifrável.
No fim, estamos acostumados ao nosso próprio jeito de fazer as coisas, e outros jeitos parecem estranhos.
Ainda assim, camadas adicionais de indireção normalmente se justificam dentro da arquitetura geral; em casos razoáveis, pode ser difícil perceber seu valor olhando apenas localmente.
“O toque leve e a quietude das primeiras linguagens de programação sempre dão prazer. Há pouco texto, mas muita coisa acontece. Programas antigos não se leem como uma discussão com o compilador, mas como uma conversa silenciosa entre um pesquisador eloquente e uma máquina colega bem treinada. Quem diria que a sofisticação compraria todo esse ruído?” — Dick Gabriel
Acabamos vendo os dois extremos: bases de código espalhadas demais por excesso de abstração e bases de código quase sem abstração nenhuma, mais próximas de scripts feitos apenas para atingir um objetivo; ambas são difíceis de trabalhar.
Vi muitos scripts em Python, JS e PHP escritos com a atitude de “não importa, só me dê o resultado que eu quero”, e pessoas que trabalham com código todos os dias precisam de abstrações que ajudem na colaboração e na resiliência.
O meme do começo foi realmente engraçado para mim, como um programador C em recuperação.
É divertido ver casos em que uma linguagem é distorcida desse jeito; em C há oportunidades de sobra para isso, e é interessante ver isso também em Go.
Dizem que é “uma piada antiga de programação dos tempos em que C e suas linguagens derivadas dominavam”, mas ainda vivemos nesses tempos.
É irônico que nesta thread estejam criticando isso como um exemplo clássico de excesso de abstração.
O motivo de variáveis com três asteriscos serem raras em C não é que cadeias de ponteiros sejam raras, mas que raramente é preciso manipular mais de dois níveis ao mesmo tempo.
Em linguagens como Python, Java e JavaScript, nas quais quase tudo é basicamente parecido com ponteiros por padrão, certamente existem cadeias de ponteiros muito maiores que 4.
As partes profundas normalmente ficam escondidas dentro de estruturas cujos detalhes internos você não precisa se preocupar — ou seja, ficam abstraídas.
Channels também exercem, em código concorrente, mais ou menos o papel que ponteiros exercem em código sequencial; então, se há uma falha fatal aqui, talvez ela não seja excesso de abstração, mas sim falta de abstração.
Ainda assim, não conheço a base de código; talvez
chan chanfosse exatamente a abstração certa para o que eles queriam escrever. Em linguagens com concorrência forte, como Erlang, é muito comum enviar um PID para outro PID para identificar o processo que deve receber a resposta.Mas não era bem isso: era o endereço de um array de strings. Era C, por isso foi assim, e ainda hoje, cerca de 20 anos depois, acho que era a solução mais intuitiva.
Em defesa do meu colega, provavelmente não havia comentário. Essa parte foi responsabilidade minha.
Isso me lembra um clássico atemporal do Buena Vista Social Club: https://www.youtube.com/watch?v=o5cELP06Mik
chan chan Valueouchan struct{resp chan Value}são padrões que já usei de fato em situações bem específicasEu poderia ter usado um barramento de mensagens, mas aí também teria que lidar com o barramento de mensagens
chan chané usado com bastante frequência. É o caso de enviar uma mensagem a um servidor interno ou ator e incluir na mensagem também o canal pelo qual a resposta será recebidaNa prática, em vez do
chan chanliteral que dá para encontrar com grep, acaba ficando algo comochan struct { ... alguma coisa que contém um canal ... }, mas o princípio é o mesmoÉ um padrão muito útil e considero uma das bases de Go
Porém, até onde sei, nunca usei
chan chan chanTransformar tudo isso em barramento de mensagens é exagero. As características de desempenho também são muito diferentes e, como os canais de Go são mais próximos de componentes internos de um processo do sistema operacional, não há valor em “promover” isso a um barramento de mensagens quando a comunicação é interna ao processo
[0]: https://github.com/adonovan/gopl.io/blob/master/ch8/chat/cha...
Canal de canais é um padrão normal, mas geralmente aparece mais como um canal de valores de struct, em que esse tipo struct contém um campo de canal
Pode ser usado para enviar solicitações que serão concluídas e esperar por esses valores independentemente da ordem, evitando bloqueio de cabeça de fila
Por exemplo, algo como
type request struct { params, reply chan response }: você envia a solicitação por um canal e, depois que o worker processa o trabalho, coloca o resultado no canalreplyMas o que vi ser útil foi até dois níveis; nunca vi um caso de uso que exigisse
chan chan chanHá um blog com um contraexemplo que implementa despacho dinâmico usando um canal que envia canais. Não é Go, é Limbo, mas o conceito é o mesmo. Talvez a complexidade acabe até comprovando o ponto https://ipn.caerwyn.com/2007/07/lab-78-dynamic-dispatch.html...
Isso me lembra “My favorite Erlang Program”, de Joe Armstrong
https://joearms.github.io/published/2013-11-21-My-favorite-e...