Concurrencia
La mayoría de nuestros programas están compuestos por funciones. Por ejemplo, un servidor web está compuesto por funciones que manejan cada petición y se encargan de enviar una respuesta. Normalmente esas pequeñas funciones no se ejecutan de manera simultanea, es decir, si recibimos varias peticiones a nuestro servidor web tenemos que esperar a que la primera función que maneja la primera petición se termine de ejecutar para poder ejecutar la siguiente función. Lo ideal sería que dichos subprogramas se ejecutaran al mismo tiempo para que todas las respuestas sean ejecutadas lo más rápido posible.
El proceso de hacer que los subprogramas se ejecuten al mismo tiempo se llama concurrencia.
Go soporta concurrencia usando goroutines
y channels
.
Goroutines
Son funciones o métodos que son capaces de ejecutarse concurrente (al mismo tiempo) que otras
funciones se ejecutan. Son una versión más ligera de hilos (threads), por lo que es común ver
programas de Go con muchas Goroutines
.
Ventajas de las Goroutines
- Son muy ligeras en comparación con crear un nuevo thread. Pesan solo unos kbs y pueden crecer o hacerse más pequeñas dependiendo de la necesidad de nuestro programa, en cambio el tamaño de los threads es fijo y tiene que ser especificado.
- Puede haber un thread de nuestro SO ejecutando muchas
goroutines
pero si alguna bloquea ese thread entonces otro threah es creado en nuestro SO y el resto degoroutines
que no han sido ejecutadas se mueven a ese nuevo thread. Todo esto lo maneja el motor de Go, así que nosotros como programadores no tenemos que preocuparnos por hacer todo esto. - Las
Goroutines
se comunican entre sí usandochannels
. Loschannels
están diseñados para prevenir "race conditions" cuando accedemos a espacios de memoria compartidos.
Todos los programas empiezan con una goroutine
explicita que es la función main
pero para
crear una nueva goroutine
tenemos que agregar el prefijo go
a la hora de ejecutar nuestra
función. Por ejemplo:
package main
import "fmt"
func hello() {
fmt.Println("Hello world from a Goroutine")
}
func main() {
go hello()
fmt.Println("Hello from main")
}
Si ejecutamos el ejemplo anterior vemos que estamos ejecutando la función go hello()
esto
crea una goroutine
pero nuestro programa se ejecuta y solo imprime "Hello from main". No
hay ningún problema con nuestro código, es totalmente normal que no veamos "Hello world from
a Goroutine". El motivo por el que sucede esto es que nuestro programa ejecuta la función
go hello()
de forma concurrente, es decir, ejecuta la función go hello()
y luego sigue
ejecutando el resto de métodos y funciones dentro de la función main
, el problema es que
nuestro programa llega a la linea que imprime "Hello from main" y ve que ya no tiene más
nada para ejecutar y se cierra.
Si queremos hacer que nuestro programa espere a que se terminen de ejecutar nuestras goroutine
tenemos que aprender un poco sobre channels
.
Channels
Los channels
son la forma en la que nos podemos comunicar con nuestras goroutine
. Son
como una tubería que envía datos de un lado a otro.
Cada channel
tiene un tipo asociado a él, este tipo es el tipo de datos que podremos
comunicar a través de él. El valor 0 de los channels
es nil
. Para crear un channel
tenemos que usar la función make
. Por ejemplo:
package main
import "fmt"
func main() {
canalInt := make(chan int)
canalString := make(chan string)
fmt.Printf("%T", canalInt)
fmt.Printf("%T", canalString)
}
En el ejemplo anterior estamos creando un channel
de tipo int
usando make(chan int)
que si imprimimos su tipo verémos que nos devuelve chan int
. Y otro channel
de
tipo string
que si imprimimos su tipo verémos que nos devuelve chan string
.
Enviando y recibiendo datos desde un channel
Para enviar datos desde un channel
usamos la siguiente sintaxis:
data := <- a
En el codigo anterior recibimos los datos del channel a
y se lo asignamos a la variable
usando `data := <- a.
Para enviar datos a un channel
usamos la siguiente sintaxis:
a <- data
En el código anterior decimos que queremos enviar el valor de data
a nuestro channel a
usando a <- data
.
Una forma fácil de entender este código es ver hacia donde apunta la flecha <-
. Si apunta
a nuestro channel
estamos enviado datos y si apunta desde nuestro channel
estamos recibiendo
datos.
Cerrando un channel
Para prevenir que nuestro programa tenga memory leaks
tenemos que cerrar nuestros channels
una vez dejemos de usarlos. Para cerrar un channel
tenemos que usar la palabra close
.
Por ejemplo:
package main
import (
"fmt"
)
func print(c chan string) {
c <- "Hello, from a Goroutine"
close(c)
}
func main() {
canal := make(chan string)
go print(canal)
if value, ok := <- canal; !ok {
fmt.Println("Error closing the channel")
} else {
fmt.Println(value)
}
}
Como podemos observar en este caso nuestro channel
retorna dos valores, value
y ok
. El
ok
lo podemos usar para verificar que nuestro channel
se ha cerrado correctamente. En
nuestra función print
estamos recibiendo un channel
y al terminar de enviar información
a este estamos usando la función close(c)
para cerrar nuestro channel
.