Índice del contenido

Go: introducción a las goroutines y concurrencia

Go: introducción a las goroutines y concurrencia

Como te mencioné en la introducción al lenguaje de programación go: go es un lenguaje especializado en la concurrencia. Es un lenguaje que fue diseñado para manejar múltiples tareas de manera asíncrona. Esta entrada trata sobre los channels o canales de go.

Concurrencia no es paralelismo

Antes de empezar, recuerda que paralelismo y concurrencia son diferentes. Este post es muy pequeño para tratar un tema tan amplio, sin embargo hay dos recursos que quiero destacar:

Cito una frase del primer recurso que, a mi parecer, resume bastante bien la diferencia:

Un programa es concurrente si puede soportar dos o más acciones en progreso.

Un programa es paralelo si puede soportar dos o más acciones ejecutándose simultáneamente.

Felipe Restrepo Calle

Si aún así te parecen confusos y no entiendes la diferencia, dale una leída a esos posts y deberías estar listo para seguir adelante.

Corrutinas en go

Una corrutina, en go, es una función o método que se ejecuta concurrentemente junto con otras funciones o métodos. En go, a las corrutinas se les conoce como goroutines o gorutinas. Incluso, la función principal, main, se ejecuta dentro de una.

Las goroutines son usadas en patrones de diseño, como el patrón de diseño worker pool

Para generar una goroutine agregamos el keyword go antes de una función. Lo anterior programará la función para su ejecución asíncrona.

func write(texto string) {
fmt.Println(texto)
}
fmt.Println("hey")
go write("hey again")
// hey

En el caso anterior, debido a su naturaleza asíncrona, la goroutine no detiene la ejecución del código. Lo anterior implica que el cuerpo de la función main continua su ejecución y nuestra goroutine nunca llega a ejecutarse.

Funcionamiento de las goroutines en go

¿Pero entonces? ¿cómo le hacemos para que nuestra goroutine se ejecute? La aproximación ingenua sería usar un sleep para pausar la ejecución del código. Esto, como ya sabes, es un sinsentido. ¡No podemos estar poniéndo sleeps en todos lados, el flujo del programa se ralentizaría innecesariamente!

// NO LO HAGAS
time.Sleep(1 * time.Second)

Una mejor aproximación sería crear un WaitGroup o grupo de espera.

WaitGroups en go

Un WaitGroup detendrá la ejecución del programa y esperará a que se ejecuten las goroutines.

Internamente, un WaitGroup funciona con un contador, cuando el contador esté en cero la ejecución del código continuará, mientras que si el contador es mayor a cero, esperará a que se terminen de ejecutar las demás goroutines.

var wg sync.WaitGroup

wg.Wait()
fmt.Println("Si el contador del waitgroup es mayor que cero se continuará con esta función.")

¿Y como cambiamos el valor del contador?

Para incrementar y decrementar el contador del WaitGroup usaremos los métodos Add y Done, respectivamente.

El método Add

El método Add incrementa el contador del WaitGroup en n unidades, donde n es el argumento que le pasamos.

El truco está en llamarlo cada vez que ejecutemos una goroutine.

wg.Add(1)
go write("Hey")

El Método Done

El método Done se encarga de disminuir una unidad del contador del WaitGroup. Lo llamaremos para avisarle al WaitGroup que la goroutine ha finalizado y decremente el contador en uno.

func write(texto string, wg *sync.WaitGroup) {
	fmt.Println(texto)
	wg.Done()
}

Recuerda que la instancia del WaitGroup (wg *) necesita pasarse por referencia o de otra manera no accederemos al WaitGroup original.

func write(texto string, wg *sync.WaitGroup) {
    fmt.Println(texto)
    defer wg.Done()
}

Tip: usa defer sobre el método Done para garantizar que sea lo último que se ejecute.

Funcionamiento de un grupo de espera en go

Una vez que el contador de wg.Wait se vuelve cero, se continua la ejecución del programa.

var wg sync.WaitGroup
wg.Add(1)
go escribirEnCanal("Ge", &wg)
wg.Wait()

Funciones anónimas en goroutines

Cuando se usan gorutinas, es bastante común utilizar funciones anónimas para evitar declarar una función nueva.

go func() {
}()

Recuerda que los paréntesis que aparecen tras el cuerpo de la función ejecutan la función anónima que declaramos y también reciben sus argumentos.

go func(text string) {
}("Texto")

Más recursos sobre goroutines

Para finalizar te dejo algunos otros recursos sobre gorutinas que puedes consultar.

Eduardo Zepeda
Desarrollador web, entusiasta de los sistemas GNU/Linux y el Software Libre. Py, Ts y Go, pero abierto a otras opciones como el Rustaceanismo. Creo en las bondades de las criptodivisas más allá de la especulación monetaria.
Leer más