Concurrencia en Golang I
Tomas Francisco Lingotti
Posted on October 19, 2022
En esta serie de posts, vamos a estar explicando varios temas relacionados a la concurrencia en Golang, desde cómo está pensada en el runtime hasta implementar patrones para resolver problemas.
En estas series, vamos a repasar los siguientes conceptos:
|1. | Modelo de concurrencia
|2. | Goroutines y canales
|3. | Wait Groups
|4. | Select y Worker pools
|5. | ErrGroup (error groups)
Ya que esta es la primera entrega, acá vamos.
Modelo de concurrencia
A diferencia de otros lenguajes, en Go tenemos goroutines (go rutinas) que nos permiten multiplexar trabajo por sobre un hilo de ejecución. Si bien eso suena a nada especial, la idea por detrás es que no vamos a crear Threads (hilos) directamente en el sistema operativo, sino que el runtime scheduler se va a hacer cargo de crear rutinas que vivan en un thread, de esa manera vamos a tener una relación MxN con hilos y rutinas. Por cada Thread vamos a poder ejecutar N goroutines.
Communicating Sequential Processes (o SCP) es el modelo teórico escrito y documentado por el científico Tony Hoare y donde Go basa su estructura principal. El paper no lo vamos a explicar en profundidad, primero por su complejidad y segundo porque Go solo toma algunos conceptos, no todo, pero pueden revisarlo aca si estan muy interesados en ver el corazón del proyecto, pero resumiendolo bastante, podemos concluir que es posible resolver concurrencia con dos primitivos que son:
- Entrada
- Salida
En un flujo donde podemos dejar plasmada de forma atómica una rutina, decimos que una entrada es información que puede sufrir un proceso para tener una salida. O sea una unidad básica de trabajo.
Si reunimos estos puntos, en una pieza de software quiere decir que también lo podemos llevar a un proceso de ejecución concurrente.
Dijimos que solo tomó prestados algunos conceptos, del paper inicial para el desarrollo del modelo de concurrencia y dentro de esa abstracción conceptual vemos el resultado tangible que se expresa en la semántica de Go con 2 mecanismos clave:
- Goroutines (rutinas)
- Canales
Y capaz (esto a mi entender solamente) el
select
como herramienta built-in para sincronizar.
Go Scheduler
Es una parte muy importante del runtime de Go, es código C estáticamente linkeado dentro del binario que vamos a ejecutar. Sabemos que Go en su fase de compilación, nos genera un único archivo ejecutable en la máquina contenedora. Dentro del runtime van a coexistir las llamadas al sistema operativo como por ejemplo, creación de threads.
Dentro del Scheduler, una sección muy importante es el goroutine scheduler, que es quien se encargue de el procesamiento de las rutinas, conozca de su ciclo de vida y sea quien decida qué rutina ejecutar en cada turno, es por eso que nosotros como desarrolladores no podemos garantizar el orden de ejecucion y/o finalizacion de las goroutines.
En esta imagen vemos como es la comunicación desde el ejecutable para con el Kernel del SSOO.
Las goroutines son similares a los hilos del SSOO pero mucho más livianas ya que ocupan menos recursos y son manejadas por Go, en cambio los threads son propiedad del SSOO y su implementación cambia entre por ejemplo Windows y MacOS.
Otra diferencia es la comunicación entre ellos, para que dos goroutines se comuniquen, existe la herramienta built-in en Go conocida como channels, en cambio para los threads no existe algo nativo que sirva con el mismo propósito.
El tiempo que tardan en crearse también es muy distinto, ya que un thread necesita un stack de un MB de memoria en un Kernel std y muchos valores de los registros asociados para poder idenficarlo, mientras que una goroutine tiene un espacio en memoria de < 2 kb.
Aun así, las rutinas de Go están basadas en los threads, corren si o si en uno de ellos, es decir, no tenemos goroutinas en el sistema operativo, por eso podemos tener N rutinas por cada thread.
Conclusiones
Como conclusión en alto nivel, tenemos por un lado, el modelo de concurrencia de Golang está basado en entrada, proceso y salida, muy simple pero no menos efectivo.
Go tiene 2 técnicas para manejar concurrencia, goroutines y canales. Tiene varios otros más para sincronización, que ya vamos a ir revisando.
Goroutines y Threads se parecen en muchos aspectos, pero no son lo mismo. Las rutinas viven en los threads y por esa razón (principalmente) son mucho más livianas. En un programa Go podemos tener M threads X N goroutines (por ejemplo 2 threads y 100 goroutines). Cómo vemos en la primera imagen.
Por último, la arquitectura de Go que al compilar, además de todo nuestro código en el programa, va a hacer un static link de varias cosas más, entre ellas el runtime que va a ser quien se encargue del manejo pesado de las goroutines. Como su ciclo de vida, ejecución, "turnos", etc, y también otras operaciones con el sistema operativo.
Espero que les guste y hasta la próxima, con la siguiente entrega.
PS: sponsoreame aca si te gusto el contenido
Posted on October 19, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.