1 puntos por GN⁺ 2024-08-29 | 1 comentarios | Compartir por WhatsApp

Introducción

  • Estamos escribiendo Dolt, la primera base de datos SQL con control de versiones del mundo, en el lenguaje Go
  • Como la mayoría de los codebases de Go, usamos canales y goroutines para implementar la ejecución concurrente
  • Como la programación concurrente suele ser difícil, por lo general usamos métodos simples e intuitivos
  • Sin embargo, heredamos de otro proyecto de código abierto un código que usa canales de una manera muy original
var c chan chan struct{}
  • Esto implementa un patrón de fan-out entre goroutines trabajadoras pasando canales entre distintas goroutines
  • Este enfoque era difícil de entender y también complicado de manejar al considerar fugas de goroutines
  • Al final reescribimos este código y eliminamos chan chan struct{}

Por qué hacer algo así

  • Existe un viejo chiste de programación de la época en que C y sus lenguajes derivados dominaban
  • A muchas personas les costaba entender los punteros
  • Como Go también es un lenguaje derivado de C, puede hacer el mismo tipo de cosas
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
}
  • Este código compila e imprime i is now 100
  • En Go se puede hacer lo mismo usando canales

El programador Go de 4-chan

  • Vamos a escribir un programa que use 4 niveles de indirección de canales
  • El canal de nivel superior se declara como 4-chan
_4chan := make(chan chan chan chan int)
  • El valor que se envía por este canal es un 3-chan
_3chan := make(chan chan chan int)
  • En cada nivel de indirección se crean productores según un factor de ramificación fijo
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)
    }()
  }
}
  • Con los consumidores se hace lo mismo
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)
    }
  }
}
  • Finalmente se llega a la etapa donde se envía el valor real
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
        }()
      }
    }()
  }
}
  • El consumidor suma los valores recibidos
var sum = &atomic.Int32{}

func receive(c chan int) {
  for s := range c {
    logrus.Debug("received int")
    sum.Add(int32(s))
  }
}
  • Juntamos todo y lo ejecutamos
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())
}
  • Este programa calcula la quinta potencia de un número de la forma más distribuida posible

Comentarios

  • Hay muchas razones para no hacer esto en código real: dificultad de implementación y depuración, orgullo personal, críticas de tus compañeros, etc.
  • Pero resulta interesante porque es muy divertido y además funciona
  • Una razón práctica es que, si envías canales dentro de canales, luego es muy difícil cerrarlos

Conclusión

  • Si tienes preguntas u opiniones sobre patrones divertidos de concurrencia en Go, puedes conversar con nuestro equipo y con otros usuarios de Dolt en Discord

Resumen de GN⁺

  • Este artículo trata sobre un patrón de concurrencia original usando canales en el lenguaje Go
  • Aunque es ineficiente para usarlo en código real, resulta interesante a nivel conceptual
  • Muestra cómo se pueden aprovechar las funciones de concurrencia de Go en proyectos como Dolt
  • Proyectos con funciones similares incluyen PostgreSQL y MySQL

1 comentarios

 
GN⁺ 2024-08-29
Opinión en Hacker News
  • Como científico, al trabajar con ingenieros de software profesionales, muchas de las cosas que hacen no se entienden

    • He visto que una sola línea de código se invoca pasando por 4 "funciones de interfaz"
    • Cada función está en un archivo y carpeta distintos, lo que vuelve muy cansada la lectura del código
    • Después de entrar varios niveles, uno se pregunta si alguna vez llegará a la parte que realmente hace los cálculos
  • Quiero dejar un comentario irrelevante y de bajo esfuerzo

    • El meme de los primeros párrafos me dio risa como programador de C
    • Me gusta ver variaciones extrañas de los lenguajes, y es interesante verlo en Go
  • Los viejos chistes de programación de la época en que C y sus lenguajes derivados dominaban siguen siendo vigentes

  • Me hace pensar en la música clásica de Buena Vista Social Club

  • He usado el patrón "chan chan Value" o "chan struct{resp chan Value}" en ciertas situaciones

    • Se podría haber usado un bus de mensajes, pero entonces uno termina teniendo que manejar el bus de mensajes
  • Los canales de canales son un patrón común, y normalmente aparecen como un campo de un tipo struct que es un canal

    • Se envía una solicitud, y después de que el trabajador termina la tarea, coloca el resultado en el canal de respuesta
    • Una forma como type request struct { params, reply chan response }
    • Dos canales son útiles, y nunca he visto más de tres canales
  • Un blog con la opinión contraria sobre usar canales para implementar un mecanismo de despacho dinámico

    • Se usa en el lenguaje Limbo, y es el mismo concepto que en Go
    • Enlace al blog
  • Hace recordar "My favorite Erlang Program" de Joe Armstrong

  • Al hacer clic en el enlace esperaba otra cosa

    • Como no soy programador de Go, no capté el chiste de inmediato
  • En código de LabVIEW uso una forma similar para recibir datos de respuesta asíncrona

    • En lugar de volcar la respuesta en una cola, se pasa un mensaje que incluye un canal de evento de callback
    • Desperdicia memoria, pero como se cierra al responder después de un solo uso, resulta eficiente