Liviana, consume poca memoria, tiene baja latencia: conozcamos las goroutines.
El lenguaje Go, que tiene soporte integrado para la programación paralela, presenta flujos livianos que se ejecutan en segundo plano. A diferencia de los hilos que existen en la mayoría de los otros lenguajes, son más baratos en términos de memoria ocupada, interacción entre hilos, tienen baja latencia y un tiempo de inicio rápido. ¿Quieres una descripción detallada de esta entidad? ¡Lee el artículo!
Definición de hilo en programación
Cualquier aplicación que funcione, desde el punto de vista técnico, es un proceso o, más bien, una ejecución secuencial de un conjunto de instrucciones. Estas instrucciones se ejecutan en orden: primero la primera, luego la segunda, luego la siguiente y así sucesivamente. Este proceso tiene un principio y un fin.
Un hilo separado es similar a un proceso y también tiene un comienzo, una secuencia de acciones definida y un final. Pero, a diferencia de un proceso, un hilo no es un programa y no puede funcionar por sí solo. En cambio, selecciona una parte de esta secuencia de acciones del programa y la ejecuta como una aplicación separada. La verdadera ventaja de este concepto es que puede haber varios hilos, y cada uno puede realizar su tarea simultáneamente con otros en un solo programa.
Un ejemplo simple de multihilos es un navegador web. Después de todo, en él puedes descargar archivos simultáneamente, desplazarte hacia abajo por las páginas, escribir algo y escuchar música. Técnicamente, un hilo puede denominarse proceso ligero, ya que requiere menos memoria que un proceso en un entorno multiprocesador. Linux generalmente los generaliza llamándolos tareas (pueden ser procesos o hilos).
Sin embargo, todavía hay una diferencia entre ellos.
- Proceso: parte de un programa en ejecución, con recursos del sistema asignados específicamente para él (tiempo de procesador, memoria, etc.).
- Hilo: es una forma de ejecutar este proceso.
¿Qué son las Goroutines?
Las goroutines son una mejora adicional del concepto de hilo, o para decirlo más simple, son funciones que pueden funcionar en paralelo con otras funciones similares en un mismo espacio de direcciones. Y las han mejorado tanto que se han convertido en una entidad separada. En un entorno multiprocesador, la creación y el mantenimiento de un proceso dependen en gran medida del sistema operativo base. Los procesos consumen recursos del sistema operativo y no los comparten entre nodos. Los hilos, aunque más ligeros que los procesos, debido al uso compartido de recursos (entre hilos pares), requieren un tamaño de pila grande, casi 1 MB. Además, la pila debe multiplicarse por el número de hilos.
Además, su cambio requiere la restauración de registros, como contadores de programa, punteros de pila, registros de coma flotante, etc. Debido a esto, el costo de mantenimiento de un proceso o hilo es bastante alto. Además, en los casos en que los datos son compartidos por nodos pares, surgen costos adicionales para la sincronización de datos. Aunque los gastos generales de cambio entre tareas están optimizados al máximo, la creación de nuevas tareas aún requiere más recursos. A veces, esto reduce considerablemente el rendimiento de la aplicación, incluso si los hilos se denominan livianos.
La ventaja de las goroutines es que no dependen del sistema operativo base, sino que existen en el espacio virtual del entorno de ejecución de Go. Como resultado, cualquier optimización de goroutine depende menos de la plataforma en la que funciona. Comienzan a funcionar con una capacidad de pila inicial de solo 2-4 KB y, junto con los canales, admiten modelos de paralelismo de procesos secuenciales interactivos (CSP), donde los valores se pasan entre acciones independientes. Estas acciones, como ya habrás adivinado, se llaman goroutines.
Cómo crear una goroutine en Go
Los desarrolladores deben comprender que las goroutines superan a los hilos solo cuantitativamente. Cualitativamente son iguales. Al iniciar un programa en Golang, la primera goroutine llama a la función principal y, por eso, a menudo se la llama goroutine principal. Si queremos crear otras goroutines nuevas, debemos usar el operador go. Por ejemplo, para llamar a una función en Golang escribimos así:
myfunc()
Aquí, después de que se haya llamado, volveremos al punto de llamada. Ahora escribiremos:
go myfunc()
El prefijo go llama a la función en una nueva goroutine y esta (función) se ejecutará de forma asíncrona con la sección de código que la llamó. Y si tomamos aproximadamente un promedio de 4 KB de capacidad de pila por goroutine, con una memoria operativa de 4 GB, podremos crear alrededor de 800,000.
Sin embargo, no debes abusar de ellas, ya que solo serán útiles en los siguientes casos:
- Cuando necesitamos asincronía. Por ejemplo, al trabajar con redes, discos, bases de datos, etc.
- Con un tiempo de ejecución largo de la función, cuando podemos ganar algo cargando otros núcleos.
El entorno de ejecución de Go, que funciona en segundo plano, inicia un conjunto de goroutines, utilizando un programador que las distribuye entre las máquinas. Luego crea un hilo que procesa todas las goroutines, y el máximo está determinado por la variable GOMAXPROCS.
Consideremos un ejemplo simple para comprender mejor el funcionamiento de las goroutines:
func main() {
i:= 10
go fmt.Printf("1. El valor de la variable i es %d\n", i)
i++
go fmt.Printf("2. El valor de la variable i es %d\n", i)
go func() {
i++
go fmt.Printf("3. El valor de la variable i es %d\n", i)
}()
i++
go fmt.Printf("4. El valor de la variable i es %d\n", i)
time.Sleep(1000000)
}
Ejecutar el código anterior en tu editor dará como resultado lo siguiente:
4. El valor de la variable i es 12
3. El valor de la variable i es 13
1. El valor de la variable i es 10
2. El valor de la variable i es 11
A continuación, se muestra el mismo código, pero sin usar goroutines:
func main() {
i:= 10
fmt.Printf("1. El valor de la variable i es %d\n", i)
i++
fmt.Printf("2. El valor de la variable i es %d\n", i)
func() {
i++
fmt.Printf("3. El valor de la variable i es %d\n", i)
}()
i++
fmt.Printf("4. El valor de la variable i es %d\n", i)
time.Sleep(1000000)
}
Aquí, el resultado esperado será el siguiente:
1. El valor de la variable i es 10
2. El valor de la variable i es 11
3. El valor de la variable i es 12
4. El valor de la variable i es 13
Cuando colocamos la palabra clave go
antes de cualquier función, crea una nueva goroutine y planifica su ejecución. La idea y el comportamiento de dicha entidad son completamente idénticos a los hilos: tiene acceso completo a los argumentos, variables globales y otros elementos de código disponibles. Además, podemos escribir goroutines con funciones anónimas. Si eliminas la llamada sleep()
en el primer ejemplo, no se mostrará la salida. Por lo tanto, la llamada a la función al final de main
hará que la goroutine principal se detenga y salga antes de que la goroutine que la generó tenga alguna posibilidad de producir una salida.
La salida de la segunda parte de nuestro ejemplo es bastante simple y consistente, lo que no se puede decir de la salida de la primera parte. Esto se debe a que el compilador limita el orden de acceso a la memoria en caso de ejecución simultánea de goroutines y, en tal caso, es imposible predecir el orden de las líneas de salida.
En resumen, podemos decir que las goroutines son un medio eficiente y que ahorra recursos para lograr la multitarea en el lenguaje de programación Go. Puedes obtener más información sobre su uso correcto en el sitio web oficial del lenguaje.