La simplicidad de la competencia en Go

La simplicidad de la competencia en Go

Varios lenguajes de programación tienen formas distintas de escribir código paralelo y/o de hacer que los procesos se diseñen de forma concurrente.

En mi opinión, Golang lo maneja de forma muy simplificada en comparación con otros stacks, ya que puedes escribir código secuencial y la comunicación entre ellos es la que hará la magia.

En este texto hablaremos de cómo Golang aborda este tema.

La competencia no es paralelismo

A veces podemos pensar que la competencia y el paralelismo son análogos, pero en realidad este pensamiento es erróneo.

La competencia no es una cuestión de ejecución, sino de diseño y estructura.

El paralelismo consiste en la ejecución simultánea de varios procesos.

En pocas palabras, se puede pensar en un sistema operativo que gestiona múltiples dispositivos de E/S (teclado, ratón, auriculares). Este software está diseñado para que todos estos dispositivos puedan trabajar juntos. Sin embargo, si el hardware solo tiene un procesador, aunque parezca que todos los dispositivos trabajan juntos, en la práctica sólo se gestiona uno de ellos a la vez. Para dejarlo más claro, el sistema operativo está diseñado de forma concurrente.

Mientras que en el caso de un ejemplo de paralelismo, podemos pensar en una arquitectura con dos procesadores y que ejecuten al mismo tiempo un cálculo súper complejo hasta llegar a la respuesta.

CSP - Comunicación de procesos secuenciales

En 1978, Tony Hoary escribió un artículo científico que contiene la base de varios conceptos utilizados hoy en Golang con respecto a la competencia.

  • El texto ilustra que el proceso es una lógica individual que toma una entrada y produce una salida. La belleza y simplicidad de este concepto permite escribir código secuencial, es decir, sin tener que cambiar la forma en que estamos acostumbrados a desarrollar para que este concepto funcione.
  • Cada proceso tiene un estado de funcionamiento local único. Esto significa que los distintos procesos no comparten sus estados con los demás. De esta manera se evitan los bloqueos (cuando se impide que un proceso continúe su ejecución) y las condiciones de carrera (cuando los procesos dependen de un orden específico para ejecutarse). Si es necesario enviar datos de un proceso a otro no se comparten, sino que se envía una copia. En consecuencia, no se acoplan los diferentes procesos. Después de todo aunque tenga información de otro proceso no puede ser manipulado por otro proceso.
  • Escala añadiendo más copias del proceso.

Varias presentaciones y documentaciones de Go lo explican así:

No te comuniques compartiendo la memoria; en su lugar, comparte la memoria comunicándote.

En otros lenguajes de programación la concurrencia es, esencialmente, tener múltiples hilos corriendo en paralelo y ejecutando alguna tarea compleja. En este modelo, es necesario que los diferentes hilos tengan acceso a alguna estructura de datos compartida entre ellos (lista, cola, diccionario). Esta compartición hace necesario bloquear un trozo de memoria específico para que dos o más hilos no accedan y modifiquen la misma memoria al mismo tiempo. Todo esto causa varios problemas como las condiciones de carrera explicadas anteriormente.

En Golang, por defecto no se comparte la memoria al otro proceso sino una copia de la misma. Los componentes implicados (pensando en un emisor y un receptor) esperarán hasta que los datos lleguen a su destino antes de continuar la ejecución. Esta espera obliga a la sincronización entre los procesos respecto a los datos que se comunican.

Goroutines

Después de los diversos conceptos explicados anteriormente, vamos a sumergirnos en el mundo de Go y entender en la práctica los conceptos de su conjunto de herramientas relacionados con la concurrencia. Asumiré que ya entiendes la sintaxis básica de go, funciones, variables, bucles, etc.

Una goroutine o gorrutina es según la documentación:

[un hilo ligero gestionado por el tiempo de ejecución de Go.

¿Recuerdas cuando hablamos de procesos con estado propio? Goroutine ofrece esa capacidad.

Imagina que quieres construir un escuchador de hashtags para monitorizar los mensajes de Twitter. En el código siguiente tenemos la función getMessages, encargada de imprimir hipotéticamente el último mensaje del hashtag pasado a través de la variable hashtag en twitter. También tenemos un time.Sleep(time.Second) que hace que el mensaje se imprima cada un segundo, de lo contrario, como el for está en un bucle infinito (con el condicional true) sólo verías una inundación gigante en tu terminal.

package main

import (
	"fmt"
	"strconv"
	"time"
)

func getMessages(hashtag string, socialMedia string) {
	for i := 1; true; i++ {
		time.Sleep(time.Second)
		fmt.Println("The last message that we got from twitter on #" + hashtag + ". Number " + strconv.Itoa(i))
	}
}

func main() {
	getMessages("cats", "twitter")
	getMessages("cats", "facebook")
}

Salida en el terminal:

➜ El último mensaje que recibimos de twitter en #cats. Número 1
➜ El último mensaje que recibimos de twitter en #cats. Número 2
➜ El último mensaje que recibimos de twitter en #cats. Número 3
➜ El último mensaje que recibimos de twitter en #cats. Número 4
➜ El último mensaje que recibimos de twitter en #cats. Número 5

Pero ahora, no solo queremos ver los mensajes de Twitter, sino los mensajes de varias redes sociales. En este caso, añadiremos una variable socialMedia a la función getMessages y también cambiar la función getMessages para mostrar el nuevo parámetro.

package main

import (
	"fmt"
	"strconv"
	"time"
)

func getMessages(hashtag string, socialMedia string) {
	for i := 1; true; i++ {
		time.Sleep(time.Second)
		fmt.Println("The last message that we got from " + socialMedia + " on #" + hashtag + ". Number " + strconv.Itoa(i))
	}
}

func main() {
	getMessages("cats", "twitter")
	getMessages("cats", "facebook")
}

Salida en el terminal:

➜ El último mensaje que recibimos de twitter en #cats. Número 1
➜ El último mensaje que recibimos de twitter en #cats. Número 2
➜ El último mensaje que recibimos de twitter en #cats. Número 3
➜ El último mensaje que recibimos de twitter en #cats. Número 4
➜ El último mensaje que recibimos de twitter en #cats. Número 5

¿Notas algo extraño? El fragmento getMessages("gatos", "facebook") no fue ejecutado. Esto ocurrió porque Go estaba demasiado ocupado con getMessages("gatos", "twitter"). Una forma de resolver este problema es comenzar uno de estos fragmentos de código con una gorrutina. De esta manera se ejecutarán en diferentes hilos.

package main

import (
	"fmt"
	"time"
)

func getMessages(hashtag string, socialMedia string) {
	for i := 1; true; i++ {
		time.Sleep(time.Second)
		fmt.Println("The last message that we got from " + socialMedia + " on #" + hashtag + ". Number " + strconv.Itoa(i))
	}
}

func main() {
	go getMessages("cats", "twitter")
	getMessages("cats", "facebook")
}

Salida en el terminal:

➜ El último mensaje que recibimos de twitter en #cats. Número 1
➜ El último mensaje que recibimos de facebook sobre #gatos. Número 1
➜ El último mensaje que recibimos de twitter en #cats. Número 2
➜ El último mensaje que recibimos de facebook sobre #gatos. Número 2
➜ El último mensaje que recibimos de facebook sobre #gatos. Número 3
➜ El último mensaje que recibimos de twitter en #cats. Número 3
➜ El último mensaje que recibimos de facebook sobre #gatos. Número 4
➜ El último mensaje que recibimos de twitter en #cats. Número 4
➜ El último mensaje que recibimos de facebook sobre #gatos. Número 5
➜ El último mensaje que recibimos de twitter en #cats. Número 5
...

¡Ahora tenemos el resultado que nos gustaría! En pocas palabras , utilizamos una palabra clave go para ejecutar una función en una rutina go. Se lanzará en el fondo de la aplicación y el go runtime lo ejecutará siempre que pueda.

Canales GO

Algo que puede parecer extraño en el código anterior es el hecho de que el getMessages realmente imprime los mensajes directamente en la terminal. Hagamos un cambio para que el mensaje sea devuelto a la función principal. De esta manera, necesitamos el getMessages para comunicarse con el principal. Introduzcamos el concepto de canal.

Según el Go

Channels are a typed conduit through which you can send and receive values with the channel operator, <-.

Traducción: Los canales nos permiten enviar información entre diferentes gorrutinas. Es importante destacar que, por defecto, una de las partes (emisor y receptor) espera (bloquea) el programa hasta que la información llega al lugar deseado. Esto es lo que permite que Go se sincronice fácilmente. Ya hemos hablado de ello.

Para utilizar un canal, debes crearlo de la siguiente manera:

ch := make(chan int)

Insertemos este concepto en nuestro código anterior:

package main

import (
	"strconv"
	"time"
)

func getMessages(hashtag string, socialMedia string, out chan string) {
	for i := 1; i < 6; i++ {
		time.Sleep(time.Second)
		var message = "The last message that we got from " + socialMedia + " on #" + hashtag + ". Number " + strconv.Itoa(i)
		out <- message
	}
}

func main() {
	ch := make(chan string)
	go getMessages("cats", "twitter", ch)
	getMessages("cats", "facebook", ch)

}

Alteramos nuestra función getMessages para componer un mensaje e insertarlo dentro de nuestro canal. Importante ressaltar que la información es transmitida en la dirección indicada. En este caso, el mensaje se inserta en el canal out <- message. También limitamos nuestra plataforma para que sean enviados apenas 5 mensajes en el canal.

Ahora, necesitamos recibir la información enviada en la función principal:

package main

import (
	"fmt"
	"strconv"
	"time"
)

func getMessages(hashtag string, socialMedia string, out chan string) {
	for i := 1; i < 6; i++ {
		time.Sleep(time.Second)
		var message = "The last message that we got from " + socialMedia + " on #" + hashtag + ". Number " + strconv.Itoa(i)
		out <- message
	}
}

func main() {
	ch := make(chan string)
	go getMessages("cats", "twitter", ch)
	message := <- ch
	fmt.Println(message)
}

Para evitar un problema de bloqueo, también hemos eliminado el getMessages sin la gorrutina.

El resultado será el siguiente:

El último mensaje que recibimos de twitter en #cats. Número 1

Puedes notar que hemos perdido el efecto dentro de la función getMessages. Después de todo, solo tenemos una impresión del mensaje en la consola. Esto ocurre porque el canal ha sido leído una sola vez y la ejecución se ha detenido. Mantengamos activa la lectura del canal durante la ejecución de la función principal mediante un bucle:

package main

import (
	"fmt"
	"strconv"
	"time"
)

func getMessages(hashtag string, socialMedia string, out chan string) {
	for i := 1; i < 6; i++ {
		time.Sleep(time.Second)
		var message = "The last message that we got from " + socialMedia + " on #" + hashtag + ". Number " + strconv.Itoa(i)
		out <- message
	}
}

func main() {
	ch := make(chan string)
	go getMessages("cats", "twitter", ch)
	for {
		message := <-ch
		fmt.Println(message)
	}
}

Ahora tenemos 2 fors. El de la función getMessages que envía el mensaje 5 veces en el canal de salida y el de la función principal que no es más que un rato para seguir leyendo los mensajes a medida que se envían.

Ahora tendremos la salida:

El último mensaje que recibimos de twitter en #cats. Número 1
El último mensaje que recibimos de twitter en #cats. Número 2
El último mensaje que recibimos de twitter en #cats. Número 3
El último mensaje que recibimos de twitter en #cats. Número 4
El último mensaje que recibimos de twitter en #cats. Número 5
error fatal: todas las goroutines están dormidas - ¡bloqueo!

goroutine 1 [chan receive]:
main.main()
        main.go:21 +0xd0

El programa se ejecutó como se esperaba, pero se produjo un bloqueo. ¿Por qué?

El bucle for de la función principal intentó seguir leyendo el canal después de que se cerrara y eso perjudica una de las reglas cuando trabajamos con esta funcionalidad. Es importante recordar las siguientes reglas:

  • Los canales sólo deben cerrarse en el lado del emisor.
  • El receptor siempre sabe cuándo se cierra un canal, pero el emisor no.
  • Si cierras un canal mientras otra gorrutina intenta enviar datos al canal, el tiempo de ejecución se bloqueará.

Para solucionar este problema, podemos cerrar manualmente el canal.

package main

import (
	"fmt"
	"strconv"
	"time"
)

func getMessages(hashtag string, socialMedia string, out chan string) {
	for i := 1; i < 6; i++ {
		time.Sleep(time.Second)
		var message = "The last message that we got from " + socialMedia + " on #" + hashtag + ". Number " + strconv.Itoa(i)
		out <- message
	}
	close(out)
}

func main() {
	ch := make(chan string)
	go getMessages("cats", "twitter", ch)
	for {
		message, ok := <-ch
		if !ok {
			break
		}
		fmt.Println(message)
	}
}

A continuación, cerraremos el canal manualmente con la función close. Cuando lo usamos, el canal devuelve otra variable que nos dice si el canal está cerrado; lo llamamos ok. También insertamos una comprobación en el bucle for de la función principal para detener el bucle si está cerrado.

Vamos a repetirlo:

El último mensaje que recibimos de twitter en #cats. Número 1
El último mensaje que recibimos de twitter en #cats. Número 2
El último mensaje que recibimos de twitter en #cats. Número 3
El último mensaje que recibimos de twitter en #cats. Número 4
El último mensaje que recibimos de twitter en #cats. Número 5

¡Muy bien! Ahora ya no tenemos un punto muerto. Podemos simplificar aún más utilizando la palabra clave range en nuestra función for main:

package main

import (
	"fmt"
	"strconv"
	"time"
)

func getMessages(hashtag string, socialMedia string, out chan string) {
	for i := 1; i < 6; i++ {
		time.Sleep(time.Second)
		var message = "The last message that we got from " + socialMedia + " on #" + hashtag + ". Number " + strconv.Itoa(i)
		out <- message
	}
	close(out)
}

func main() {
	ch := make(chan string)
	go getMessages("cats", "twitter", ch)
	for message := range ch {
		fmt.Println(message)
	}
}

Tenemos un código más sucinto y el mismo resultado al ejecutarlo:

El último mensaje que recibimos de twitter en #cats. Número 1
El último mensaje que recibimos de twitter en #cats. Número 2
El último mensaje que recibimos de twitter en #cats. Número 3
El último mensaje que recibimos de twitter en #cats. Número 4
El último mensaje que recibimos de twitter en #cats. Número 5

Es posible que se pierda cuando eliminamos nuestra otra llamada a getMessages. En este momento estamos recuperando sólo la información de Twitter, cuando en realidad nos gustaría recuperar tanto twitter como Facebook. Aquí es donde entra el concepto de selección, pero lo dejaremos para otro texto.

Esto es sólo una introducción al mundo de la competición con Go. Aquí trabajamos sólo con casos hipotéticos. después de todo no hay conexión con las APIs reales de las redes sociales lo que haría todo más interesante. En un caso real, nuestro programa recibiría estos mensajes y realizaría alguna acción con cada mensaje recibido.

Espero que te haya gustado el artículo, ¡hasta la próxima! :)

⚠️
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Revelo.

Revelo Content Network da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.