43 - Goroutines - Channels

Como hemos visto las Goroutines se ejecutan en forma concurrente (si hay varios procesadores pueden estar ejecutándose en forma simultanea), en el problema anterior cuando ordenamos dos vectores en forma concurrente no teníamos ningún problemas de sincronización ya que son dos problemas completamente independientes.

En muchas situaciones la ejecución de una Goroutine depende del estado de ejecución de otra Goroutine.

Cuando dos o más Goroutines deben comunicarse o acceder a recursos comunes el lenguaje Go propone el mecanismo de Channels (canales) para que se comuniquen e intercambien recursos.

Un Channel es un mecanismo de comunicación que permite a una Goroutine enviar valores a otro goroutine.

Cuando se crea un canal hay que indicar el tipo de dato que transportará el mismo: int, string, float64, un vector, un slice, un registro etc.

Para la creación de un canal utilizamos la función make. Ej:

canal := make(chan int)

Aparece una nueva palabra clave llamada "chan", seguida de esta debemos indicar el tipo de dato que transportará el canal.

Las dos actividades fundamentales que podemos hacer con una variable de tipo channel es enviar el dato y recibir el dato.

Para enviar un dato disponemos del lado izquierdo del operador <- el nombre del canal:

canal<- [valor a enviar]

Para recuperar el dato disponemos del lado derecho del operador <- el nombre del canal:

[variable que recibe] <-canal

Una Goroutine envía un dato y otra Goroutine lo recibe.

Problema 1:

Confeccionar dos Goroutines, una que dentro de un for infinito incremente un contador de uno en uno y se lo envíe a otra Gorutine para que lo imprima.
El envío del contador entre una Goroutine y otra hacerlo mediante un Channel.

Programa: ejercicio168.go

package main

import "fmt"

func contar(canal chan int) {
    x := 0
    for {
        canal<- x
        x++
    }
}

func imprimir(canal chan int) {
    var valor int
    for {
        valor = <-canal
        fmt.Println(valor)
    }
}

func main() {
    canal := make(chan int)
    go contar(canal)
    go imprimir(canal)
    var fin string
    fmt.Scanln(&fin)
}

En este problema no podemos definir una variable global y que una Goroutine la incremente y la otra la muestre sin que haya una sincronización entre las mismas, podría una de las funciones imprimir el mismo valor varias veces o la que lo incrementa hacerlo varias veces antes que la otra lo muestre.

Estas dos funciones deben sincronizarse para que cuando la que incrementa el contador lo haga le avise a la que imprime que lo muestre.

Si ejecutamos este programa veremos los números a partir de 0 de 1 en 1 hasta que el operador presione la tecla "enter" y finalice el programa.

En la main creamos un Channel llamado canal de tipo de dato int:

    canal := make(chan int)

Lanzamos las dos Gorutines enviando como parámetro el Channel:

    go contar(canal)
    go imprimir(canal)

Para que la main no finalice llamamos a la función Scanln que nos permite ingresar una tecla "enter" para finalizar el programa:

    var fin string
    fmt.Scanln(&fin)

Desde este momento que se iniciaron las dos Goroutines pueden pasar varias situaciones, imaginemos que comienza a ejecutarse primero la función imprimir, donde se define una variable int y entra a un for infinito. Dentro del for llega la línea donde trata de extraer el dato del canal:

        valor = <-canal

Suponiendo que todavía no inició la función contar el canal esta vacío. En esta línea se bloquea la Goroutine imprimir hasta que haya algo en el canal.

En algún momento comienza a ejecutarse la función contador donde iniciamos una variable entera en cero y entramos a un for infinito:

func imprimir(canal chan int) {
    var valor int
    for {

Dentro del for cargamos en el Channel el contador:

        canal<- x

Hasta que el valor del Channel no sea recuperado desde la otra Goroutine (imprimir) la función contar permanece bloqueada. Una vez que imprimir saca el valor:

        valor = <-canal

La función contar continúa su ejecución con el incremento de la variable local x en 1 y nuevamente guardando dicho valor en el Channel.

Como podemos comprobar la comunicación entre las dos Goroutines es en forma sincrónica. Una función carga en el canal y espera a que sea retirado el valor, la otra función espera a que haya algo en el canal y lo retira.