Una goroutine es una función que se ejecuta de forma concurrente gestionada por el runtime de Go. Es ligera, barata de crear y una de las razones por las que Go encaja tan bien en servidores.
Hemos llegado a una de las partes más conocidas de Go: la concurrencia.
Go hace sencillo iniciar una tarea concurrente, aunque coordinarla correctamente sigue requiriendo cuidado.
Vamos a conocer las goroutines y el papel del runtime al repartirlas entre los hilos del sistema operativo.
Qué es una goroutine
Una Goroutine es una función que se ejecuta de manera concurrente con otras funciones.
No es lo mismo que un hilo del sistema operativo. Es una unidad de ejecución concurrente que el runtime de Go planifica sobre uno o varios hilos.
El programa inicia la tarea y el planificador decide cuándo y en qué hilo puede ejecutarse.
Sintaxis con la palabra clave go
Solo tenemos que añadir la palabra clave go delante de una llamada a función.
package main
import (
"fmt"
"time"
)
func decirHola() {
fmt.Println("¡Hola desde la goroutine!")
}
func main() {
// Ejecución síncrona (normal)
decirHola()
// Ejecución concurrente
go decirHola()
fmt.Println("Fin del main")
}La trampa del main
Si ejecutas el código de arriba tal cual, es muy probable que no veas el mensaje de la goroutine. O que lo veas a veces sí y a veces no.
¿Por qué?
Porque la función main es, en sí misma, una goroutine (la principal). Cuando main termina, el programa muere inmediatamente, matando a todas las goroutines que estuvieran corriendo a medio gas.
Para “ver” el resultado (de forma chapucera, solo por ahora), podemos pausar el main:
func main() {
go decirHola()
fmt.Println("Esperando un poco...")
time.Sleep(1 * time.Second) // Chapuza temporal
fmt.Println("Fin")
}Usar time.Sleep para esperar a una goroutine es una mala práctica. En producción usaremos WaitGroups o Canales, que veremos en los siguientes artículos.
Goroutines frente a hilos del sistema
La diferencia principal está en la pila dinámica y el planificador del runtime. Esto permite crear muchas más goroutines que hilos del sistema, aunque cada una sigue consumiendo memoria y otros recursos.
Tamaño de la pila
- Hilo del sistema: su pila inicial y sus costes dependen del sistema operativo y de la biblioteca utilizada, pero suelen ser mayores.
- Goroutine: comienza con una pila pequeña que crece y se reduce según las necesidades. El tamaño exacto es un detalle de implementación y puede cambiar entre versiones.
Coste de creación y destrucción
- Hilo del sistema: crearlo implica al sistema operativo y suele tener un coste apreciable.
- Goroutine: la crea el runtime con menos trabajo, por lo que resulta mucho más barata, aunque no gratuita.
Cambios de contexto
El procesador solo puede ejecutar una cosa a la vez por núcleo. Para dar la ilusión de multitarea, cambia rápidamente entre procesos.
- Hilo del sistema: el núcleo del sistema operativo interviene en su planificación.
- Goroutine: el runtime puede suspenderla y ejecutar otra sin convertir cada tarea en un hilo independiente.
El planificador
Go usa un planificador propio basado en tres piezas: G (goroutines), M (hilos del sistema operativo) y P (procesadores lógicos disponibles para ejecutar Go).
El runtime reparte muchas goroutines sobre un conjunto de hilos del sistema operativo. Para código Go que ejecuta CPU, GOMAXPROCS limita cuántos procesadores lógicos P pueden ejecutar trabajo simultáneamente.
- Si una goroutine espera una operación, el runtime puede dejar que otras goroutines avancen. Para red suele apoyarse en su poller; una llamada de sistema bloqueante puede requerir otro hilo.
- La operación sigue siendo bloqueante para esa goroutine, aunque no tenga por qué detener al resto del programa.
Este modelo encaja bien con servidores que atienden muchas operaciones de entrada y salida concurrentes. Una goroutine por petición no implica un hilo por petición.
Concurrencia frente a paralelismo
Conviene distinguir ambos conceptos: la concurrencia organiza tareas que pueden progresar de forma independiente; el paralelismo ejecuta trabajo simultáneamente.
- Concurrencia: Una sola persona haciendo café y tostadas. Intercala tareas (pone café, pone pan, saca café, saca pan). Avanza en ambas, pero solo hace una cosa a la vez.
- Paralelismo: Dos personas; una hace el café y otra hace las tostadas a la vez.
Las goroutines nos dan concurrencia. Si hay varios procesadores lógicos disponibles y las tareas pueden avanzar a la vez, el runtime también puede ejecutarlas en paralelo.