1 puntos por GN⁺ 2024-08-29 | 1 comentarios | Compartir por WhatsApp
  • DoltHub creó un ejemplo en tono de broma que anida de forma deliberadamente excesiva los canales, muy usados en la concurrencia de Go, enviando canales a través de canales
  • En código heredado real había un chan chan struct{}, usado en un patrón de fan-out que pasaba un nuevo canal a una goroutine worker, pero se reescribió porque era difícil de razonar y administrar
  • El ejemplo extiende el chiste de “programador 4-star” de int**** en lenguajes de la familia C al chan de Go, usando _4chan := make(chan chan chan chan int) como canal de nivel superior
  • Con factor = 3, en cada nivel de canales se ramifican producers y consumers, y al sumar los valores int finales imprime 243, que es 3 a la 5.ª potencia
  • En la práctica no es adecuado por la dificultad de implementación y depuración, el manejo del cierre de canales, la necesidad de sync.WaitGroup y las fugas de goroutines; el ejemplo depende de time.Sleep() en lugar de una lógica de finalización

Casos de anidamiento de canales surgidos en Dolt

  • DoltHub está escribiendo en Go Dolt, la primera base de datos SQL con control de versiones del mundo
  • Como en una base de código típica en Go, usa channels y goroutines para implementar ejecución concurrente
  • Como la programación concurrente ya es difícil de por sí, normalmente los canales y las goroutines se manejan de una forma simple e intuitiva
  • En una época, un código tomado de otro proyecto open source tenía un canal que enviaba canales, así:
    • var c chan chan struct{}
  • Esta estructura pasaba canales entre goroutines para implementar un patrón de fan-out de goroutines worker
    • El canal intermedio actuaba como intermediario, pasando el canal recién creado al worker que hacía el trabajo real
    • Funcionaba, pero era difícil de razonar y manejar, sobre todo considerando las fugas de goroutines
    • Ese código se reescribió y el chan chan struct{} desapareció

La versión en Go del chiste de “programador 4-star”

  • En la época en que C y sus lenguajes derivados se usaban ampliamente, existía el chiste del “programador 4-star” sobre principiantes a los que les costaba entender los punteros
  • Un ejemplo representativo es código que usa varios niveles de indirección de punteros, como int****
  • Como Go también deriva en gran medida de C, se puede escribir código del mismo estilo con punteros
    • Pasando sucesivamente *int, **int, ***int, ****int
    • Si en la última función se ejecuta ****i = 100, el programa imprime i is now 100
  • Como Go tiene chan, que C no tiene, se puede extender el mismo chiste a la indirección mediante canales

Calcular una quinta potencia con canales de 4 niveles

  • El canal de nivel superior se declara así
    • _4chan := make(chan chan chan chan int)
  • Como los identificadores de Go no pueden empezar con un número, el ejemplo usa el nombre _4chan
  • El valor que se envía a _4chan es un canal de 3 niveles
    • _3chan := make(chan chan chan int)
  • De la misma manera se desciende por la jerarquía hasta llegar finalmente a un canal de valores, chan int
  • En cada nivel de indirección se crean producers según la constante factor
    • En el ejemplo, const factor = 3
    • sendChanChanChan inicia un producer de canales de 3 niveles como goroutine
  • Del lado de los consumers, en cada nivel se recibe el canal entrante y se inician tantos consumers del siguiente nivel como indique factor
    • receiveChanChanChan recibe _3chan desde _4chan e inicia consumers de canales de 3 niveles

Envío y suma de valores en el último nivel

  • En el nivel más bajo ya no se envían canales, sino valores int reales
  • La función send envía _1chan a _2chan y luego inicia producers de enteros según factor
  • Cada producer de enteros vuelve a crear tantas goroutines como indique factor para ejecutar _1chan <- 1
  • El consumer suma el entero recibido a la variable global sum
    • sum se declara como atomic.Int32
    • receive(c chan int) recibe un valor del canal y ejecuta sum.Add(int32(s))

Resultado de ejecución y cantidad de ramificaciones

  • El programa completo crea _4chan, inicia las capas de envío y recepción cada una como una goroutine, y luego espera durante 500 * time.Millisecond
  • La salida del ejemplo es la siguiente
    • 3 ^ 5: 243
  • Este programa es un ejemplo generalizado para calcular la quinta potencia de un número de la forma más distribuida posible
  • El ejemplo ejecutable se puede ver en Go Playground, y la versión con resaltado de sintaxis está en GitHub Gist
  • Para usar un factor más grande, quizá haya que aumentar el tiempo de Sleep para que la ejecución pueda terminar
  • Si se activan los logs, se puede verificar la cantidad de ramificaciones de cada capa de producción y consumo de canales
    • starting 3chan producer: 3 veces
    • starting 2chan producer: 9 veces
    • starting 3chan consumer: 9 veces
    • starting 2chan consumer: 27 veces
    • starting chan producer: 27 veces
    • starting 1chan consumer: 81 veces
    • starting int producer: 81 veces
    • sending int: 243 veces
    • received int: 243 veces

Por qué evitarlo en código real

  • Este enfoque es complicado de implementar y depurar en código real
  • Al enviar canales a través de canales, se vuelve difícil determinar cuándo cerrar cada canal
  • En un caso de uso real habría que cerrar los canales, pero para agregar lógica de finalización habría que rastrear si terminaron todos los envíos por todos los canales
  • Para implementar el manejo de finalización, habría que agregar sync.WaitGroup por todas partes, y eso haría que el ejemplo de broma fuera difícil de leer
  • El ejemplo final se simplificó usando time.Sleep() en lugar de lógica de finalización, y dejando muchas fugas de goroutines

1 comentarios

 
GN⁺ 2024-08-29
Opiniones de Hacker News
  • Como científico que trabaja de cerca con ingenieros de software profesionales de verdad, mucho de lo que hacen se ve así, y me cuesta muchísimo entender por qué lo hacen
    He visto que una sola línea de código, antes de ser llamada realmente, pasa en orden por 4 funciones de interfaz, y esas funciones están dispersas en distintos archivos de distintas carpetas
    Por eso leer qué hace el código se vuelve agotador y, después de entrar varios niveles, uno empieza a dudar de si está mirando el lugar correcto y de si algún día llegará a donde ocurre el cálculo real

    • Esto es una práctica realmente mala, y se parece más a la forma en que un ingeniero junior demasiado entusiasmado escribe software
      La sensación de que es excesivo y confuso no está equivocada; cuando uno escribe código “interesante” por primera vez, puede parecer técnicamente complejo e incluso elegante, pero dentro de software que realmente tiene que crecer se convierte en una pesadilla técnica
      Una vez pasé casi 2 años limpiando malos usos de canales en código Go; el problema es que los canales rara vez son realmente necesarios, pero al principio se pueden usar fácilmente para muchas cosas
      El criterio para saber si se están usando bien los canales es poder responder que no a “¿no se puede hacer con una llamada directa a una función?” y “¿no se puede hacer con un wait group o un mutex?”, y que sí a “¿la ganancia en concurrencia/paralelismo es lo bastante grande como para justificar la complejidad de depurar código concurrente?”
    • Hay cosas peores. Este no es un ejemplo demasiado exagerado: https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...
      A veces hay una falta aterradora de comprensión y capacidad donde debería haberlas
      En la universidad hice pair programming unos 30 minutos con un doctor en ciencias de la computación, y fue bastante revelador
      No entendía nada de software, al punto de señalar que no estábamos comprobando que el tamaño de una estructura de datos de la biblioteca estándar no fuera negativo
      Dicho eso, a veces estas estructuras tienen una razón de ser. A veces son realmente razonables, y otras veces son una forma de lidiar con una base de código demencial creada por quienes llegaron antes
    • En un extremo del espectro de quienes programan están los científicos, y en el otro, los ingenieros de software. Lo único que puede salvarnos es el equilibrio
      He leído código usado en papers de investigación, y como la matemática teórica suele estar más allá de mi comprensión, cuando uno entra al código para ver la lógica, muchas veces resulta todavía peor e ilegible
      Al final, estamos acostumbrados a nuestra propia forma de hacer las cosas, y las otras formas se sienten extrañas
    • La sobreingeniería es una causa común. Las soluciones simples muchas veces están escondidas y son difíciles de encontrar
      Aun así, las capas adicionales de indirección suelen justificarse dentro de la arquitectura completa, y si el caso es razonable, puede ser difícil ver su valor mirándolo solo de forma local
      “El toque ligero y la calma de los primeros lenguajes de programación siempre dan gusto. No hay mucho texto, pero se logra mucho. Los programas antiguos no se leen como una discusión con el compilador, sino como una conversación tranquila entre un investigador elocuente y un colega mecánico bien entrenado. ¿Quién habría sabido que la sofisticación compraría todo este ruido?” — Dick Gabriel
    • En sentido contrario, aunque estoy de acuerdo en que los ingenieros de software, cuando no saben bien, llevan la abstracción demasiado lejos, tampoco tengo en especial alta estima por el código escrito por gente que no es ingeniera de software de profesión
      Es ver ambos extremos: bases de código demasiado dispersas por exceso de abstracciones, y bases de código casi como scripts para cumplir un objetivo, sin ninguna abstracción; ambas son difíciles de trabajar
      Muchos scripts en Python, JS y PHP estaban escritos con la actitud de “ya, dame el resultado que quiero”, y quienes trabajan con código todos los días necesitan abstracciones que ayuden a la colaboración y la resiliencia
  • El meme del principio realmente me dio risa como programador C en recuperación
    Es divertido ver casos en los que se retuerce un lenguaje de esta manera; C está lleno de oportunidades para eso, y es interesante verlo también en Go

  • Dice “un viejo chiste de programación de la época en que C y sus derivados dominaban”, pero todavía vivimos en esa época

  • Es irónico que en este hilo se critique esto como el ejemplo típico de exceso de abstracción
    La razón por la que rara vez se ven variables de tres estrellas en C no es que las cadenas de punteros sean raras, sino que no suele hacer falta manipular más de dos niveles al mismo tiempo
    En lenguajes como Python, Java y JavaScript, donde casi todo es básicamente parecido a un puntero por defecto, sin duda hay cadenas de punteros de longitud muy superior a 4
    Lo profundo suele estar oculto dentro de estructuras cuyos interiores no hace falta tener en cuenta; es decir, está abstraído
    Como los canales cumplen en código concurrente un papel más o menos equivalente al de los punteros en código secuencial, si aquí hay un defecto fatal, quizá no sea exceso de abstracción, sino más bien falta de abstracción
    Aun así, como no conozco la base de código, quizá chan chan era exactamente la abstracción adecuada para lo que intentaban escribir. En lenguajes con mucha concurrencia como Erlang, es muy común enviar un PID a otro PID para identificar el proceso que debe recibir la respuesta

    • Al inicio de mi carrera todavía recuerdo que me regañaron por un commit con tres estrellas. Era algo así como: quién necesita un puntero a un puntero a un puntero
      Pero no era eso: era la dirección de un arreglo de strings. Era C, y aun ahora, unos 20 años después, sigo pensando que era la solución más intuitiva
      En defensa de mi colega, probablemente no había comentarios. Esa parte fue mi responsabilidad
  • Me recuerda al clásico atemporal de Buena Vista Social Club https://www.youtube.com/watch?v=o5cELP06Mik

  • chan chan Value o chan struct{resp chan Value} son patrones que he usado realmente en situaciones muy específicas
    También se podría usar un bus de mensajes, pero entonces habría que lidiar con el bus de mensajes

    • chan chan se usa con bastante frecuencia. Es el caso de enviar un mensaje a un servidor interno o a un actor, incluyendo en el mensaje el canal por el que se recibirá la respuesta
      En la práctica, más que un chan chan literal que puedas encontrar con grep, suele tener la forma de chan struct { ... algo que contiene un canal ... }, pero el principio es el mismo
      Es un patrón muy útil y lo considero una de las bases de Go
      Eso sí, hasta donde sé, nunca he usado chan chan chan
      Cambiar todo esto por un bus de mensajes es excesivo. Sus características de rendimiento también son muy distintas y, como los canales de Go son más bien componentes internos de un proceso del sistema operativo, no vale la pena “actualizar” a un bus de mensajes para comunicación dentro del proceso
    • Probablemente sea un patrón parecido al del servidor de chat concurrente de gopl. Al principio sorprende un poco, pero aun así es bastante legible
      [0]: https://github.com/adonovan/gopl.io/blob/master/ch8/chat/cha...
    • Yo también lo he usado. Es útil para implementar en Go una semántica parecida a Promise
  • Un canal de canales es un patrón válido, pero normalmente aparece más seguido como un canal de valores de estructura, donde ese tipo de estructura incluye un campo de canal
    Puede usarse para enviar solicitudes que se completarán y permitir esperar esos valores sin depender del orden, evitando así el bloqueo de cabeza de línea
    Por ejemplo, se envía una solicitud por un canal como type request struct { params, reply chan response }, y luego el worker procesa el trabajo y coloca el resultado en el canal reply
    Pero lo útil para mí llegó hasta dos niveles; nunca he visto un caso de uso que requiera chan chan chan

  • Hay un blog con un contraejemplo que implementa despacho dinámico mediante un canal que envía canales. No es Go sino Limbo, pero el concepto es el mismo. Tal vez la complejidad termine demostrando el punto https://ipn.caerwyn.com/2007/07/lab-78-dynamic-dispatch.html...

    • Para quienes tengan curiosidad, Limbo es un lenguaje precursor de Go y se usaba en el sistema operativo Inferno, descendiente de Plan 9
  • Me viene a la mente “My favorite Erlang Program” de Joe Armstrong
    https://joearms.github.io/published/2013-11-21-My-favorite-e...