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
Opinión en Hacker News
Como científico, al trabajar con ingenieros de software profesionales, muchas de las cosas que hacen no se entienden
Quiero dejar un comentario irrelevante y de bajo esfuerzo
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
Los canales de canales son un patrón común, y normalmente aparecen como un campo de un tipo
structque es un canaltype request struct { params, reply chan response }Un blog con la opinión contraria sobre usar canales para implementar un mecanismo de despacho dinámico
Hace recordar "My favorite Erlang Program" de Joe Armstrong
Al hacer clic en el enlace esperaba otra cosa
En código de LabVIEW uso una forma similar para recibir datos de respuesta asíncrona