Skip to content

Go routines y channels

Published:

Go routines

Go tiene una funcionalidad que permite crear nuevos contextos de ejecución, externos al contexto main() que es la función principal de todos los programas escritos en Go, pero dependientes. El nuevo contexto permite ejecutar código concurrentemente esto sirve mucho para no bloquear el contexto principal y que este puede seguir ejecutando otras funciones mientras otra rutina realiza otro trabajo.

Loading graph...

import (
"fmt"
"time"
)

func calculo(){
 time.Sleep(2 * time.Minutes)
}

func calculo2(){
	time.Sleep(5 * time.Minutes)
}

// main_routine
func main() {

// Goroutine
go func(){
	calculo2() // calculo de 5 minutos
}()

calculo() // calculo de 2 minutos
}

// main_routine termina correctamente y Goroutine es terminada inesperadamente

Si la función main() crea una goroutine que realizará un cálculo que demorará 5 minutos. Y la función main() realizará otro cálculo que demorara 2 minutos, una vez que la función main() termine su ejecución el programa finalizará, sin esperar a que la goroutine de 5 minutos haya finalizado su cálculo, esto quiere decir que hay que declarar, de forma explicita, que se debe esperar a la goroutine pendiente, es aquí donde entra la librería sync que nos entrega herramientas para declarar de forma explicita, cuando hay que esperar a que finalice una goroutine.

La librería sync nos entrega herramientas que permiten bloquear la ejecución del contexto de ejecución principal main() o cualquier otro contexto de ejecución, en este caso, Goroutines.

Loading graph...

La librería sync posee una herramienta para crear un grupo de espera llamado en ingles WaitGroup, que define una cola de grupos de espera, estos pueden ser agregados con el método Add() y podemos bloquear un contexto con Wait() esperando a que se vayan confirmando terminaciones de grupos de espera con el método Done()

import (
"fmt"
"sync"
"time"
)

func calculo(){
 time.Sleep(2 * time.Minutes)
}

func calculo2(){
	time.Sleep(5 * time.Minutes)
}
// main_routine
func main() {

var wg sync.WaitGroup

wg.Add(1) // Añadimos un grupo de espera

// Goroutine
go func(){
  defer wg.Done() 
	calculo2() // calculo de 5 minutos
}()

calculo() // calculo de 2 minutos

wg.Wait() // esperamos a que todos los grupos de espera terminen
}

// main_routine y Goroutine terminan correctamente

Quizás ahora surge la pregunta ¿ Cómo podría comunicar dos o más Goroutines que necesitan enviarse información? Para eso están los Channels

Channels

El uso fundamental de los channels es enviar y/o recibir información entre Goroutines. Por defecto los canales son “unbuffered”, es decir, que no almacenan el dato que pasa a través de ellos, pero existe la posibilidad de almacenar datos si se declara de forma explicita, siendo estos “buffered” channels. Cabe destacar que los canales receptores datos bloquean la ejecución de la Goroutine hasta que los datos sean obtenidos por el receptor, en el caso contrario si el canal envía datos pero no hay quien los reciba, este canal no bloqueará la ejecución de la Goroutine, por tanto, sólo se ignorará.

Unbuffered channels

Para entender mejor el uso fundamental de un canal sin buffer (unbuffered channel), podemos hacer la analogía con un portal, un portal sólo expone una entrada y una salida, pero no existe un intermedio, es decir, no existe una forma de que el dato quede entre medio de la salida o la entrada.

Loading graph...

En el grafico anterior el canal pertenece la main_routine, y las goroutines se comunican a través de este canal, independiente de que las goroutine se ejecuten de forma concurrente, cabe destacar que el cáculo que se realiza en la Goroutine2 estará bloqueando esa rutina, ya que el canal esta definido como receptor, por lo tanto, bloqueará el contexto hasta que lleguen datos a través de él.

package main

import (
"fmt"
"sync"
)

func calculo1() int {
	return 1 + 1
}

func calculo2(value int) int {
	return value + 2
}

func main() {
	
	ch := make(chan int)

	var wg sync.WaitGroup

	wg.Add(1)	
	
	// We create first the go routine that receives the data, to be ready
	go func(){	
		result1 := <- ch // Recibimos el dato y efectuamos el cálculo
		//       ^ esta operación es bloqueante, dado a que debemos esperar al dato
		// para seguir con la ejecución de la goroutine
		
		fmt.Printf("finalRes %v", calculo2(result1))
		
	wg.Done()
	}()
	
	// We create the sender go routine to send the data to the ready goroutine
	// wich have the receiver goroutine
	go func(){
		
		resCalc := calculo1()
		
		ch <- resCalc // Enviamos resCalc a tráves del canal 'ch'
		// ^ esta operacion no es bloqueante, por lo tanto si no hay
		// receptor, no entrara en "deadlock"
		
	}()

	wg.Wait()	
}