Más información sobre los canales almacenados en búfer

Completado

Como ha aprendido, los canales no se almacenan en búfer de forma predeterminada. Esto significa que aceptan una operación de envío solo si hay una de recepción. De lo contrario, el programa se bloqueará, quedando en espera indefinidamente.

Hay ocasiones en las que se necesita ese tipo de sincronización entre goroutines. Sin embargo, puede haber ocasiones en las que solo necesite implementar la simultaneidad, entonces no necesita restringir el modo en que las goroutines se comunican entre sí.

Los canales almacenados en búfer envían y reciben datos sin bloquear el programa porque un canal almacenado en búfer se comporta como una cola. Puede limitar el tamaño de esta cola al crear el canal, de la siguiente manera:

ch := make(chan string, 10)

Cada vez que se envía algo al canal, el elemento se agrega a la cola. A continuación, una operación de recepción quita el elemento de la cola. Cuando el canal está lleno, cualquier operación de envío simplemente espera hasta que haya espacio para almacenar los datos. Por el contrario, si el canal está vacío y hay una operación de lectura, se bloquea hasta que haya algo que leer.

Este es un ejemplo sencillo para comprender los canales almacenados en búfer:

package main

import (
    "fmt"
)

func send(ch chan string, message string) {
    ch <- message
}

func main() {
    size := 4
    ch := make(chan string, size)
    send(ch, "one")
    send(ch, "two")
    send(ch, "three")
    send(ch, "four")
    fmt.Println("All data sent to the channel ...")

    for i := 0; i < size; i++ {
        fmt.Println(<-ch)
    }

    fmt.Println("Done!")
}

Al ejecutar el código se ve la siguiente salida:

All data sent to the channel ...
one
two
three
four
Done!

Podría decir que no hemos hecho nada diferente y estaría en lo cierto. Pero vamos a ver qué ocurre cuando cambia la variable size a un número menor (incluso puede probar con uno mayor), como se muestra a continuación:

size := 2

Cuando vuelva a ejecutar el programa, obtendrá el siguiente error:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.send(...)
        /Users/developer/go/src/concurrency/main.go:8
main.main()
        /Users/developer/go/src/concurrency/main.go:16 +0xf3
exit status 2

La razón es que las llamadas a la función send son secuenciales. No está creando una goroutine. Por lo tanto, no hay nada que poner en cola.

Los canales están estrechamente conectados a las goroutines. Sin otra goroutine que recibe datos del canal, todo el programa podría bloquearse indefinidamente. Como ha visto, esto puede ocurrir.

Ahora vamos a hacer algo interesante. Se creará una goroutine para las dos últimas llamadas (las dos primeras encajan correctamente en el búfer) y realizar una ejecución de bucle for cuatro veces. Este es el código:

func main() {
    size := 2
    ch := make(chan string, size)
    send(ch, "one")
    send(ch, "two")
    go send(ch, "three")
    go send(ch, "four")
    fmt.Println("All data sent to the channel ...")

    for i := 0; i < 4; i++ {
        fmt.Println(<-ch)
    }

    fmt.Println("Done!")
}

Al ejecutar el programa, funcionará según lo previsto. Se recomienda que, al usar los canales, siempre se usen goroutines.

Ahora se probará el caso en el que se crea un canal almacenado en búfer con más elementos de los que necesitará. Se usará el ejemplo utilizado antes para comprobar las API y crear un canal en búfer con un tamaño de 10:

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    start := time.Now()

    apis := []string{
        "https://management.azure.com",
        "https://dev.azure.com",
        "https://api.github.com",
        "https://outlook.office.com/",
        "https://api.somewhereintheinternet.com/",
        "https://graph.microsoft.com",
    }

    ch := make(chan string, 10)

    for _, api := range apis {
        go checkAPI(api, ch)
    }

    for i := 0; i < len(apis); i++ {
        fmt.Print(<-ch)
    }

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}

Al ejecutar el programa se obtiene la misma salida que antes. Puede experimentar cambiando el tamaño del canal por números inferiores o superiores, y el programa seguirá funcionando.

Canales almacenados y no almacenados en búfer

Llegados a este punto, es posible que se pregunte cuándo usar un tipo u otro. Todo depende de cómo desea que la comunicación fluya entre las goroutines. Los canales no almacenados en búfer se comunican sincrónicamente. Garantizan que cada vez que envía datos, el programa se bloquee hasta que alguna lee el canal.

Por el contrario, los canales almacenados en búfer desacoplan las operaciones de envío y recepción. No bloquean un programa, pero debe tener cuidado, ya que podría acabar provocando un interbloqueo (como hemos visto anteriormente). Al usar canales no almacenados en búfer, puede controlar el número de goroutines que se pueden ejecutar simultáneamente. Por ejemplo, puede que esté realizando llamadas a una API y desee controlar el número de llamadas que hace cada segundo. Si no, podría bloquearse el programa.

Direcciones de los canales

En Go, los canales tienen otra característica interesante. Cuando los canales se usan como parámetros para una función, se puede especificar si un canal está diseñado para enviar o recibir datos. A medida que crezca el programa, es posible que tenga demasiadas funciones, así que se recomienda documentar el propósito de cada canal para utilizarlo correctamente. O quizás está escribiendo una biblioteca y desea exponer un canal como de solo lectura para mantener la coherencia de los datos.

Para definir la dirección del canal, hágalo de forma similar a cuando lea o reciba datos. Se tiene que hacer cuando esté declarando el canal en un parámetro de función. La sintaxis para definir el tipo de canal como un parámetro en una función es la siguiente:

chan<- int // it's a channel to only send data
<-chan int // it's a channel to only receive data

Cuando se envían datos a través de un canal pensado para ser de solo recepción, se obtiene un error al compilar el programa.

Vamos a usar el siguiente programa como ejemplo de dos funciones, una que lee los datos y otra que los envía:

package main

import "fmt"

func send(ch chan<- string, message string) {
    fmt.Printf("Sending: %#v\n", message)
    ch <- message
}

func read(ch <-chan string) {
    fmt.Printf("Receiving: %#v\n", <-ch)
}

func main() {
    ch := make(chan string, 1)
    send(ch, "Hello World!")
    read(ch)
}

Al ejecutar el código se ve la siguiente salida:

Sending: "Hello World!"
Receiving: "Hello World!"

El programa clarifica el intento de cada canal en cada función. Si intenta usar un canal para enviar datos en un canal cuya finalidad es solo recibir datos, obtendrá un error de compilación. Por ejemplo, intente realizar algo similar a lo siguiente:

func read(ch <-chan string) {
    fmt.Printf("Receiving: %#v\n", <-ch)
    ch <- "Bye!"
}

Al ejecutar el programa, verá el siguiente error:

# command-line-arguments
./main.go:12:5: invalid operation: ch <- "Bye!" (send to receive-only type <-chan string)

Es mejor tener un error de compilación que usar indebidamente un canal.

Multiplexación

Por último, se verá cómo interactuar con más de un canal simultáneamente mediante la palabra clave select. En ocasiones, querrá esperar a que se produzca un evento cuando trabaja con varios canales. Por ejemplo, podría incluir alguna lógica para cancelar una operación cuando haya una anomalía en los datos que el programa está procesando.

Una instrucción select funciona como una instrucción switch, pero para los canales. Bloquea la ejecución del programa hasta que recibe un evento que se va a procesar. Si obtiene más de un evento, elige uno de forma aleatoria.

Un aspecto esencial de la instrucción select es que finaliza su ejecución después de procesar un evento. Si desea esperar a que se produzcan más eventos, es posible que deba usar un bucle.

Vamos a usar el siguiente programa para ver select en acción:

package main

import (
    "fmt"
    "time"
)

func process(ch chan string) {
    time.Sleep(3 * time.Second)
    ch <- "Done processing!"
}

func replicate(ch chan string) {
    time.Sleep(1 * time.Second)
    ch <- "Done replicating!"
}

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    go process(ch1)
    go replicate(ch2)

    for i := 0; i < 2; i++ {
        select {
        case process := <-ch1:
            fmt.Println(process)
        case replicate := <-ch2:
            fmt.Println(replicate)
        }
    }
}

Al ejecutar el código se ve la siguiente salida:

Done replicating!
Done processing!

Observe que primero ha finalizado la función replicate, por lo que en primer lugar verá su salida en el terminal. La función "main" tiene un bucle porque la instrucción select finaliza en cuanto recibe un evento, pero aún estamos esperando a que finalice la función process.