1 pontos por GN⁺ 2024-08-29 | 1 comentários | Compartilhar no WhatsApp

Introdução

  • Estamos escrevendo o Dolt, o primeiro banco de dados SQL com controle de versão do mundo, na linguagem Go
  • Como na maioria das bases de código em Go, usamos canais e goroutines para implementar execução concorrente
  • Como programação concorrente geralmente é difícil, usamos métodos simples e intuitivos
  • No entanto, herdamos de outro projeto open source um código que usa canais de uma forma muito original
var c chan chan struct{}
  • Isso implementa um padrão de fan-out entre goroutines trabalhadoras, passando canais entre diferentes goroutines
  • Essa abordagem era difícil de entender e complicada de manter ao considerar vazamentos de goroutines
  • No fim, reescrevemos esse código e removemos o chan chan struct{}

Por que fazer isso

  • Existe uma velha piada de programação da época em que C e suas linguagens derivadas eram dominantes
  • Muitas pessoas tinham dificuldade para entender ponteiros
  • Como Go também é uma linguagem derivada de C, ele pode fazer a mesma coisa
func main() {
  i := 1
  setInt(&i)
  fmt.Printf("i is now %d", i)
}

func setInt(i *int) {
  setInt2(&i)
}

func setInt2(i **int) {
  setInt3(&i)
}

func setInt3(i ***int) {
  setInt4(&i)
}

func setInt4(i ****int) {
  ****i = 100
}
  • Esse código compila e imprime i is now 100
  • Em Go, é possível fazer a mesma coisa usando canais

O programador Go de 4-chans

  • Vamos escrever um programa que usa 4 níveis de indireção de canais
  • O canal de nível mais alto é declarado como um 4-chan
_4chan := make(chan chan chan chan int)
  • O valor enviado para esse canal é um 3-chan
_3chan := make(chan chan chan int)
  • Em cada nível de indireção, geramos produtores de acordo com um fator de ramificação fixo
func sendChanChanChan(c chan chan chan chan int) {
  for range factor {
    go func() {
      logrus.Debug("starting 3chan producer")
      _3chan := make(chan chan chan int)
      sendChanChan(c, _3chan)
    }()
  }
}
  • Os consumidores são tratados da mesma forma
func receiveChanChanChan(c chan chan chan chan int) {
  for _3chan := range c {
    logrus.Debug("got message from 4chan")
    for range factor {
      logrus.Debug("starting 3chan consumer")
      go receiveChanChan(_3chan)
    }
  }
}
  • Por fim, chegamos à etapa em que o valor real é enviado
func send(_2chan chan chan int, _1chan chan int) {
  _2chan <- _1chan
  for range factor {
    go func() {
      logrus.Debug("starting int producer")
      for range factor {
        go func() {
          logrus.Debug("sending int")
          _1chan <- 1
        }()
      }
    }()
  }
}
  • O consumidor soma os valores recebidos
var sum = &atomic.Int32{}

func receive(c chan int) {
  for s := range c {
    logrus.Debug("received int")
    sum.Add(int32(s))
  }
}
  • Juntando tudo, executamos assim
const factor = 3
var sum = &atomic.Int32{}

func main() {
  // logrus.SetLevel(logrus.DebugLevel)
  _4chan := make(chan chan chan chan int)
  go sendChanChanChan(_4chan)
  go receiveChanChanChan(_4chan)
  time.Sleep(500 * time.Millisecond)
  fmt.Printf("%d ^ 5: %d", factor, sum.Load())
}
  • Esse programa calcula a quinta potência de um número da forma mais distribuída possível

Comentários

  • Há muitos motivos para não fazer isso em código real: dificuldade de implementação e depuração, questões de orgulho pessoal, críticas dos colegas etc.
  • Ainda assim, é interessante porque é muito divertido e funciona
  • Um dos motivos práticos é que, ao enviar canais dentro de canais, fica muito difícil fechá-los corretamente

Conclusão

  • Se você tiver perguntas ou opiniões sobre padrões divertidos de concorrência em Go, pode conversar com nossa equipe e outros usuários do Dolt no Discord

Resumo do GN⁺

  • Este artigo aborda um padrão de concorrência original usando canais na linguagem Go
  • Embora seja ineficiente para uso em código real, é conceitualmente interessante
  • Mostra como os recursos de concorrência de Go podem ser aproveitados em projetos como o Dolt
  • Projetos com funcionalidade semelhante incluem PostgreSQL, MySQL etc.

1 comentários

 
GN⁺ 2024-08-29
Comentários no Hacker News
  • Como cientista, ao trabalhar com engenheiros de software profissionais, muita coisa que eles fazem não faz sentido para mim

    • Já vi uma única linha de código ser chamada passando por 4 "funções de interface"
    • Cada função fica em um arquivo e pasta diferentes, então ler o código fica muito cansativo
    • Depois de alguns níveis, começo a me perguntar se algum dia vou chegar na parte que realmente faz o cálculo
  • Quero deixar um comentário sem muito esforço e sem substância

    • O meme dos primeiros parágrafos foi engraçado para mim como programador C
    • Gosto de ver variações estranhas de linguagens, e foi interessante ver isso em Go
  • As velhas piadas de programação da época em que C e suas linguagens derivadas dominavam ainda continuam válidas

  • Isso me lembrou a música clássica do Buena Vista Social Club

  • Já usei o padrão chan chan Value ou chan struct{resp chan Value} em certas situações

    • Eu poderia ter usado um barramento de mensagens, mas aí passaria a ter que lidar com um barramento de mensagens
  • Canal de canal é um padrão comum e normalmente aparece na forma de um campo de um tipo struct que é um canal

    • Você envia uma requisição, e depois que o worker termina o trabalho, ele coloca o resultado no canal de resposta
    • Algo como type request struct { params, reply chan response }
    • Dois canais são úteis, e nunca vi três ou mais canais
  • Um post de blog com opinião contrária sobre usar canais para implementar um mecanismo de despacho dinâmico

    • É usado na linguagem Limbo, com o mesmo conceito do Go
    • link do blog
  • Isso me fez lembrar de "My favorite Erlang Program", do Joe Armstrong

  • Eu esperava outra coisa quando cliquei no link

    • Como não sou programador Go, não percebi a piada de imediato
  • Uso uma abordagem parecida em código LabVIEW para receber dados de resposta assíncrona

    • Em vez de despejar a resposta em uma fila, passo uma mensagem que inclui um canal de evento de callback
    • Há desperdício de memória, mas como é fechado após uma única resposta, acaba sendo eficiente