No es ningún misterio que una de las principales características por las que la gente se interesa por Go es la concurrencia. La premisa de Go con la concurrencia es hacerla sencilla y manejable para todos, cosa que no podemos decir de otros lenguajes, donde incluso ni existe la concurrencia y tenemos que buscarnos la vida de otras maneras.
Pero antes de meternos de llenos a picar, deberíamos entender bien que es la concurrencia y en qué se diferencia del paralelismo, ya que muchas veces estos términos tienden a confundirse.
Diferencias entre concurrencia y paralelismo
La concurrencia es cuando dos o más tareas pueden empezar, ejecutarse y completarse en períodos de tiempos superpuestos. Esto no quiere decir que ambos procesos corran a la misma vez. Mientras que en el paralelismo las tareas se ejecutarán literalmente a la misma vez, esto solo es posible cuando tenemos más de un procesador de otra manera es imposible realizar tareas en paralelo y posiblemente hablemos de tareas ejecutándose concurrentemente.
Gorrutinas
En Go, una tarea que se ejecuta de manera independiente se le conoce por gorrutina. Empezar una gorrutina es muy sencillo, basta con añadir la palabra reservada go
delante de una función:
package main
func main() {
go func() {
// do something concurrently
}()
}
Cada vez que usamos la palabra reservada go
estaremos ejecutando una gorrutina podríais pensar que cada gorrutina se está ejecutando a la vez, pero esto no tiene porque ser así ya que los ordenadores sólo tienen un número limitado de unidades de procesamiento.
De hecho, estos procesadores suelen pasar un tiempo en cada gorrutina antes de pasar a otra, esta técnica es conocida como time sharing. Por esto además hay que asumir que cada tarea o gorrutina se ejecutará en cualquier orden.
Podemos verlo en un simple ejemplo con los empleados de la Estrella de la Muerte, que recordemos si tuvo los problemas de seguridad que tuvo, es porque pasaban más tiempo durmiendo que trabajando:
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 5; i++ {
go sleepyStormtrooper(i)
}
time.Sleep(4 * time.Second)
}
func sleepyStormtrooper(id int) {
time.Sleep(3 * time.Second)
fmt.Printf("The Stormtrooper, number %d, is snoring\n", id)
}
¿Cuándo ejecutemos este programa cuál será el resultado? Seguro que estás esperando ver algo como esto:
¿Y si te decimos que lo que realmente devolverá es algo cómo esto?
¿No nos crees? Pruébalo tu mismo: https://play.golang.org/p/s3WoI5jLbSn
Usando canales con nuestras gorrutinas
Obviamente sabemos que la programación no siempre es tan sencilla, y tendremos que realizar programas más complicados con concurrencia, un caso muy común que nos encontraremos es el de comunicar dos gorrutinas, para esto en Go tenemos lo que se llaman channels
o canales
.
Los canales pueden ser utilizados como variables como cualquier otro tipo en Go, es decir podremos pasarlos a las funciones y ser propiedades de nuestros structs
.
Para inicializar un canal haremos uso de la función make
al igual que la utilizamos en los maps
y slices
. Además tendremos que especificar el tipo de datos que va a recibir el canal a la hora de crearlos.
c := make(chan string)
Con nuestro canal creado ya estamos listo para mandar o recibir mensajes, para ello utilizaremos el operador <-
, veamos como funciona:
Si queremos mandar datos a nuestro canal:
c <- "Hello, Darth Vader"
Y si queremos recibirlos:
m := <- c
Algo que tenéis que tener muy presente es que las operaciones con canales son bloqueantes, así que mientras un canal este abierto y haya alguna parte de nuestro código esperando recibir valores, no se ejecutará nada por debajo o en el caso de una gorrutina la mantendrá bloqueada.
Vamos a verlo mejor con nuestro ejemplo anterior, hagamos que los Stormtroopers se despierten y se pongan a trabajar.
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
for i := 0; i < 5; i++ {
go sleepyStormtrooper(i, c)
}
for i := 0; i < 5; i++ {
stormtrooper := <-c
fmt.Printf("The Stormtrooper, number %d, is awake!\n", stormtrooper)
}
close(c)
}
func sleepyStormtrooper(id int, c chan int) {
time.Sleep(3 * time.Second)
fmt.Printf("The StormTrooper, number %d, is snoring\n", id)
c <- id
}
Playground: https://play.golang.org/p/2IK4buoiIoz
Algo que debería llamarnos fuertemente la atención en el ejemplo anterior, además del uso de canales es que ha desaparecido la sentencia time.Sleep(4 * time.Second)
que había tras nuestro primer for
donde ejecutábamos las gorrutinas, esto se debe a como dijimos a que stormtrooper := <-c
es bloqueante, estaremos ahí parados hasta recibir algo en nuestro canal.
¿Qué queremos decir con bloqueante? Pues en este caso hemos declarado un canal, c := make(chan int)
que quiere decir que espera enviar y recibir un elemento cada vez, unbuffered channel
, en otro artículo ya explicaremos los buffered channels
donde podremos enviar más de un elemento, pero de momento mantengámonos básicos, al declarar que vamos a enviar y recibir un elemento, si éste no fuera enviado c <- id
se nos produciría un deadlock, con su consiguiente panic, fatal error: all goroutines are asleep - deadlock!
ya que nuestro programa se quedaría bloqueado en ese punto eternamente y no podríamos continuar nuestra ejecución.
En este ejemplo se ve muy sencillo pero los deadlocks son uno de los errores más comunes cuando trabajamos con concurrencia, pero no hay nada que unos buenos tests, no arreglen.
Hemos añadido además el cierre de nuestro canal al final de la ejecución del último for
, aunque en este caso no sería estrictamente necesario, ya que sabemos de antemano cuantas gorrutinas y cuando terminaremos de procesarlas.
Complicando los canales
Hay veces que queremos hacer diferentes ejecuciones dependiendo de lo que suceda con las gorrutinas, y como no vamos a llenar nuestro código de if/else
, Go nos provee de un switch
especial diseñado especialmente para canales.
Imaginad a Darth Vader en la Estrella de la Muerte, ¿de verdad pensáis que esperaría a que todos los Stormtroopers se despertaran? o ¿perdería la paciencia mucho antes?
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
for i := 0; i < 5; i++ {
go sleepyStormtrooper(i, c)
}
darthVaderPatience := time.After(2 * time.Second)
for i := 0; i < 5; i++ {
select {
case stormtrooper := <-c:
fmt.Printf("The Stormtrooper, number %d, is awake!\n", stormtrooper)
case <-darthVaderPatience:
fmt.Println("You've exhaust my patience")
return
}
}
}
func sleepyStormtrooper(id int, c chan int) {
time.Sleep(3 * time.Second)
fmt.Printf("The StormTrooper, number %d, is snoring\n", id)
c <- id
}
Playground: https://play.golang.org/p/K77E24sXPqC
Lo primero que hemos hecho es declarar la paciencia de nuestro querido Darth Vader, darthVaderPatience := time.After(2 * time.Second)
, la función time.After()
devuelve un canal, el cual será rellenado una vez se alcancé el tiempo especificado, es decir una vez pasen los 2 segundos ese canal recibirá información y podremos actuar en consecuencia.
Gracias al select
podremos definir diferentes casos y decidir que queremos hacer en cada uno de ellos, en este ejemplo lo que hacemos es cortar la ejecución de nuestras gorrutinas pasado 2 segundos, esto es muy útil si tenemos un proceso muy lento y queremos pararlo pasado un tiempo determinado. Así pues en el ejemplo anterior, veremos que la paciencia de Darth Vader acaba mucho antes de que cualquier Stormtrooper se despierte, ¿seguirán con vida…?
No se cuando acabarán mis gorrutinas
Muchas veces no podremos controlar cuando acabarán nuestras gorrutinas, pero necesitamos saberlo para poder actuar en consecuencia.
Para este caso podemos resolverlo de varias maneras, os vamos a explicar uno muy útil y sencillo que es el de beneficiarnos de dos gorrutinas, del select
y un bucle infinito.
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
quit := make(chan bool)
var counter int
for i := 0; i < 5; i++ {
go sleepyStormtrooper(i, c, quit, &counter)
}
go func() {
for {
select {
case stormtrooper := <-c:
fmt.Printf("The Stormtrooper, number %d, is awake!\n", stormtrooper)
}
}
}()
<-quit
fmt.Println("All the Stormtroopers are awake")
close(c)
close(quit)
}
func sleepyStormtrooper(id int, c chan int, quit chan bool, counter *int) {
time.Sleep(3 * time.Second)
fmt.Printf("The StormTrooper, number %d, is snoring\n", id)
c <- id
*counter++
if *counter >= 5 {
quit <- true
}
}
Playground: https://play.golang.org/p/K6dX4LPlFU5
El ejemplo es algo forzado, pero se entiende lo que queremos explicar, en este caso nuestro programa se estará ejecutando hasta que se cumpla la condición de que counter >= 5
. Como hemos explicado <-quit
es una acción bloqueante y se estará esperando a recibir algo para continuar, por ello hasta que no se cumpla la condición jamás se imprimirá el mensaje de que todos los Stormtroopers están despiertos. Esto puede llegar a provocar un deadlock
ya que si fallara algo y no llegamos a publicar en el canal quit
nunca terminariamos, pero en artículos venideros explicaremos como mejorar esta gestión con los buffered channels
.
A todo lo que hemos explicado del trato con canales, se le conoce como paso por mensajes, pero cuando nuestros objetos son muy muy grandes, este patrón puede ser no muy útil y se pasaría a utilizar memoria compartida, pero esto ya lo veremos en otro artículo.
Esperamos haber cubierto vuestras ansías de concurrencia por un tiempo y pongáis en práctica lo aprendido, ya sabéis que podéis dejarnos cualquier duda en los comentarios, o vía Twitter @FriendsofGoTech