Se dice que Go es el lenguaje C del siglo 21. Pienso que hay dos razones para eso: la primera, Go es un lenguaje simple; segundo, la concurrencia es un tema candente en el mundo de hoy, y Go soporta esta característica a nivel de lenguaje.
goroutines y concurrencia están integradas en el diseño del núcleo de Go. Ellas son similares a los hilos pero trabajan de forma diferente. Más de una docena de goroutines a lo mejor por debajo solo tienen 5 o 6 hilos. Go también nos da soporte completo para compartir memoria entre sus goroutines. Una goroutine usualmente usa 4~5 KB de memoria en la pila. Por lo tanto, no es difícil ejecutar miles de goroutines en una sola computadora. Una goroutine es mas liviana, más eficiente, y más conveniente que los hilos del sistema.
Las goroutines corren en el administrador de procesos en tiempo de ejecución en Go. Usamos la palabra reservada go
para crear una nueva goroutine, que por debajo es una función ( main() es una goroutine ).
go hello(a, b, c)
Vamos a ver un ejemplo.
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0; i < 5; i++ {
runtime.Gosched()
fmt.Println(s)
}
}
func main() {
go say("world") // creamos una nueva goroutine
say("hello") // actual goroutine
}
Salida:
hello
world
hello
world
hello
world
hello
world
hello
Podemos ver que es muy fácil usar concurrencia en Go usando la palabra reservada go
. En el ejemplo anterior, estas dos goroutines comparten algo de memoria, pero sería mejor si utilizáramos la receta de diseño: No utilice datos compartidos para comunicarse, use comunicación para compartir datos.
runtime.Gosched() le dice al CPU que ejecute otras goroutines, y que en algún punto vuelva.
El manejador de tareas solo usa un hilo para correr todas la goroutines, lo que significa que solo implementa la concurrencia. Si buscas utilizar mas núcleos del CPU para usar mas procesos en paralelo, tenemos que llamar a runtime.GOMAXPROCS(n) para configurar el numero de núcleos que deseamos usar. Si n<1
, esto no va a cambiar nada. Esta función se puede quitar en el futuro, para ver mas detalles sobre el procesamiento en paralelo y la concurrencia vea el siguiente articulo.
goroutines son ejecutadas en el mismo espacio de direcciones de memoria, por lo que se tiene que mantener sincronizadas si buscas acceder a la memoria compartida. ¿Cómo nos comunicamos entre diferentes goroutines? Go utiliza un muy buen mecanismo de comunicación llamado canales
(channel). Los canales
son como dos tuberías (o pipes) en la shell de Unix: usando canales para enviar o recibir los datos. El unico tipo de datos que se puede usar en los canales es el tipo channel
y la palabra reservada para eso es chan
. Ten en cuenta que para crear un nuevo channel
debemos usar la palabra reservada make
.
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
Los canales usan el operador <-
para enviar o recibir datos.
ch <- v // enviamos v al canal ch.
v := <-ch // recibimos datos de ch, y lo asignamos a v
Vemos esto en un ejemplo.
package main
import "fmt"
func sum(a []int, c chan int) {
total := 0
for _, v := range a {
total += v
}
c <- total // enviamos total a c
}
func main() {
a := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(a[:len(a)/2], c)
go sum(a[len(a)/2:], c)
x, y := <-c, <-c // recibimos de c
fmt.Println(x, y, x + y)
}
Enviando y recibimos los datos por defecto en bloques, por lo que es mucho mas fácil usar goroutines sincrónicas. Lo que quiero decir, es que el bloque en la goroutine no va a continuar cuando reciba datos de un canal vacío (value := <-ch
), hasta que otras goroutines envíen datos a este canal. Por otro lado, la goroutine por otro lado no enviara datos al canal (ch<-5
) hasta que no reciba datos.
Anteriormente hice una introducción sobre canales non-buffered channels (non-buffered channels), y Go también tiene 'buffered channels' que pueden guardar mas de un elemento. Por ejemplo, ch := make(chan bool, 4)
, aquí creamos un canal que puede guardar 4 elementos booleanos. Por lo tanto con este canal, somos capaces de enviar 4 elementos sin el bloqueo, pero la goroutine se bloqueará cuando intente enviar un quinto elemento y la goroutine no lo recibirá.
ch := make(chan type, n)
n == 0 ! non-buffer(block)
n > 0 ! buffer(non-block until n elements in the channel)
Puedes probar con este código en tu computadora y cambiar algunos valores.
package main
import "fmt"
func main() {
c := make(chan int, 2) // si cambia 2 por 1 tendrá un error en tiempo de ejecución, pero 3 estará bien
c <- 1
c <- 2
fmt.Println(<-c)
fmt.Println(<-c)
}
Podemos usar range para hacer funcionar los 'buffer channels' como una lista y un map.
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 1, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x + y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
for i := range c
no parara de leer información de el canal hasta que el canal se alla cerrado. Vamos a usar la palabra reservada close
para cerrar el canal en el ejemplo anterior. Es imposible enviar o recibir datos de un canal cerrado, puede usar v, ok := <-ch
para verificar si el canal esta cerrado. Si ok
devuelve false, esto significa que no hay datos en ese canal y este fue cerrado.
Recuerde cerrar siempre los canales productores, no los consumidores, o sera muy fácil obtener un estado de pánico o 'panic status'.
Otra cosa que deber tener que recordar es que los canales son diferentes a los archivos, y no debe cerrarlos con frecuencia, a menos que este seguro que es canal esta completamente sin uso, o desea salir del bloque donde usa 'range'.
En los ejemplos anteriores, nosotros usamos solo un canal, pero ¿cómo podemos lidiar con más de un canal? Go tiene la palabra reservada llamada select
para escuchar muchos canales.
select
de forma predeterminada es bloqueante, y este continua la ejecución solo cuando un canal tiene datos o recibió datos. Si varios canales están listos para usarse al mismo tiempo, select elegirá cual ejecutar al azar.
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x + y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
select
también tiene default
, al igual que el switch
. Cuando todos los canales no están listos para ser usados, ejecuta el default (no espera mas por el canal).
select {
case i := <-c:
// usa i
default:
// se ejecuta cuando c esta bloqueado
}
A veces la goroutine esta bloqueada, ¿pero como podemos evitar que esto, mientras tanto nos bloquee el programa? Podemos configurar para esto un timeout en el select.
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <- c:
println(v)
case <- time.After(5 * time.Second):
println("timeout")
o <- true
break
}
}
}()
<- o
}
El paquete runtime
tiene algunas funciones para hacer frente a las goroutines.
-
runtime.Goexit()
Sale de la actual goroutine, pero las funciones defer son ejecutadas como de costumbre.
-
runtime.Gosched()
Permite que el manejador de tareas ejecute otras goroutines, y en algún momento vuelve allí.
-
runtime.NumCPU() int
Devuelve el numero de núcleos del CPU
-
runtime.NumGoroutine() int
Devuelve el numero de goroutines
-
runtime.GOMAXPROCS(n int) int
Configura cuantos núcleos del CPU queremos usar
- Índice
- Sección anterior: interfaces
- Siguiente sección: Resumen