- 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 alchande 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 valoresintfinales 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.WaitGroupy las fugas de goroutines; el ejemplo depende detime.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 imprimei is now 100
- Pasando sucesivamente
- 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
_4chanes 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 sendChanChanChaninicia un producer de canales de 3 niveles como goroutine
- En el ejemplo,
- Del lado de los consumers, en cada nivel se recibe el canal entrante y se inician tantos consumers del siguiente nivel como indique
factorreceiveChanChanChanrecibe_3chandesde_4chane 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
intreales - La función
sendenvía_1chana_2chany luego inicia producers de enteros segúnfactor - Cada producer de enteros vuelve a crear tantas goroutines como indique
factorpara ejecutar_1chan <- 1 - El consumer suma el entero recibido a la variable global
sumsumse declara comoatomic.Int32receive(c chan int)recibe un valor del canal y ejecutasum.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 durante500 * 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
factormás grande, quizá haya que aumentar el tiempo deSleeppara 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 vecesstarting 2chan producer: 9 vecesstarting 3chan consumer: 9 vecesstarting 2chan consumer: 27 vecesstarting chan producer: 27 vecesstarting 1chan consumer: 81 vecesstarting int producer: 81 vecessending int: 243 vecesreceived 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.WaitGrouppor 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
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
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?”
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
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
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
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 chanera 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 respuestaPero 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 Valueochan struct{resp chan Value}son patrones que he usado realmente en situaciones muy específicasTambién se podría usar un bus de mensajes, pero entonces habría que lidiar con el bus de mensajes
chan chanse 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 respuestaEn la práctica, más que un
chan chanliteral que puedas encontrar con grep, suele tener la forma dechan struct { ... algo que contiene un canal ... }, pero el principio es el mismoEs un patrón muy útil y lo considero una de las bases de Go
Eso sí, hasta donde sé, nunca he usado
chan chan chanCambiar 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
[0]: https://github.com/adonovan/gopl.io/blob/master/ch8/chat/cha...
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 canalreplyPero lo útil para mí llegó hasta dos niveles; nunca he visto un caso de uso que requiera
chan chan chanHay 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...
Me viene a la mente “My favorite Erlang Program” de Joe Armstrong
https://joearms.github.io/published/2013-11-21-My-favorite-e...