
Lo más probable es que ya haya escuchado sobre el lenguaje de programación Go, su popularidad crece constantemente, lo cual es bastante razonable. Este lenguaje es simple, rápido y se basa en una gran comunidad. Uno de los aspectos más curiosos del lenguaje es el modelo de programación multiproceso. Las primitivas subyacentes le permiten crear programas multiproceso de manera fácil y sencilla. Este artículo está destinado a aquellos que desean aprender estas primitivas: gorutinas y canales. Y, a través de las ilustraciones, mostraré cómo trabajar con ellos. Espero que esto sea de gran ayuda para usted en su estudio posterior.
Programas únicos y multiproceso
Lo más probable es que ya haya escrito programas de subproceso único. Por lo general, se ve así: hay un conjunto de funciones para realizar diversas tareas, cada función se llama solo cuando la anterior preparó datos para ella. Por lo tanto, el programa se ejecuta secuencialmente.
Ese será nuestro primer ejemplo: el programa de extracción de minerales. Nuestras funciones buscarán, extraerán y procesarán minerales. El mineral en la mina en nuestro ejemplo está representado por listas de cadenas, las funciones las toman como parámetros y devuelven una lista de cadenas "procesadas". Para un programa de subproceso único, nuestra aplicación se diseñará de la siguiente manera:

En este ejemplo, todo el trabajo lo realiza un hilo (la gopher de Gary). Tres funciones principales: búsqueda, producción y procesamiento se realizan secuencialmente una tras otra.
func main() { theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} foundOre := finder(theMine) minedOre := miner(foundOre) smelter(minedOre) }
Si imprimimos el resultado de cada función, obtenemos lo siguiente:
From Finder: [ore ore ore] From Miner: [minedOre minedOre minedOre] From Smelter: [smeltedOre smeltedOre smeltedOre]
El diseño y la implementación simples son una ventaja de un enfoque de subproceso único. Pero, ¿qué pasa si desea ejecutar y ejecutar funciones de forma independiente? Aquí la programación multiproceso viene en su ayuda.

Este enfoque para la extracción de minerales es mucho más eficiente. Ahora varios hilos (gophers) funcionan de forma independiente, y Gary solo hace parte del trabajo. Un gopher busca mineral, el otro produce y el tercero se derrite, y todo esto es potencialmente simultáneo. Para implementar este enfoque, necesitamos dos cosas en el código: crear procesadores Gopher independientemente uno del otro y transferir mineral entre ellos. Go tiene gorutinas y canales para esto.
Gorutins
Las goroutines pueden considerarse "hilos ligeros", para crear goroutines solo necesita poner la palabra clave
go antes del código de llamada de función. Para demostrar lo simple que es, creemos dos funciones de búsqueda, llámelas con la palabra clave
go e imprima un mensaje cada vez que encuentren el "mineral" en su mina.

func main() { theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} go finder1(theMine) go finder2(theMine) <-time.After(time.Second * 5)
El resultado de nuestro programa será el siguiente:
Finder 1 found ore! Finder 2 found ore! Finder 1 found ore! Finder 1 found ore! Finder 2 found ore! Finder 2 found ore!
Como puede ver, no hay un orden en el que la función primero "encuentre mineral"; Las funciones de búsqueda funcionan simultáneamente. Si ejecuta el ejemplo varias veces, el orden será diferente. Ahora podemos ejecutar programas de subprocesos múltiples (multiesferas), y este es un progreso serio. Pero, ¿qué hacer cuando necesitamos establecer una conexión entre gorutinas independientes? Se acerca el momento de la magia de los canales.
Canales

Los canales permiten que las gorutinas intercambien datos. Este es un tipo de tubería a través del cual las gorutinas pueden enviar y recibir información de otras gorutinas.

La lectura y escritura en el canal se realiza utilizando el operador de flecha (<-), que indica la dirección del movimiento de los datos.

myFirstChannel := make(chan string) myFirstChannel <- "hello"
Ahora nuestro gopher-scout no necesita acumular mineral, puede transferirlo de inmediato utilizando canales.

Actualicé el ejemplo, ahora el código del buscador de minerales y minero es funciones anónimas. No se preocupe demasiado si no los ha encontrado antes, solo tenga en cuenta que cada uno de ellos se llama con la palabra clave
go , por lo tanto, se ejecutará en su propia rutina. Lo más importante aquí es que las gorutinas transmiten datos entre ellas utilizando el canal
oreChan . Y nos ocuparemos de las funciones anónimas más cerca del final.
func main() { theMine := [5]string{“ore1”, “ore2”, “ore3”} oreChan := make(chan string)
La conclusión a continuación demuestra claramente que nuestro Minero recibe tres veces del canal o una porción a la vez.
Miner: Received ore1 from finder Miner: Received ore2 from finder Miner: Received ore3 from finder
Entonces, ahora podemos transferir datos entre diferentes goroutines (gophers), pero antes de comenzar a escribir un programa complejo, veamos algunas propiedades importantes de los canales.
Cerraduras
En algunas situaciones, cuando se trabaja con canales, goroutin puede estar bloqueado. Esto es necesario para que las gorutinas puedan sincronizarse entre sí antes de comenzar o continuar trabajando.
Bloqueo de escritura

Cuando goroutine (gopher) envía datos a un canal, se bloquea hasta que otra goroutine lee datos del canal.
Leer bloqueo

Similar al bloqueo al escribir en un canal, goroutin puede bloquearse al leer desde un canal hasta que no se escriba nada en él.
Si las cerraduras, a primera vista, le parecen complicadas, puede imaginarlas como una "transferencia de dinero" entre dos goroutines (gophers). Cuando un gopher quiere transferir o recibir dinero, tiene que esperar al segundo participante en la transacción.
Habiendo tratado con bloqueos de rutina en los canales, analicemos dos tipos diferentes de canales: con y sin búfer. Al elegir este o aquel tipo, determinamos en gran medida el comportamiento del programa.
Canales sin búfer

En todos los ejemplos anteriores, utilizamos solo esos canales. En tales canales, solo se puede transmitir una pieza de datos a la vez (con bloqueo, como se describió anteriormente).
Canales almacenados

Las secuencias en un programa no siempre se pueden sincronizar perfectamente. Supongamos, en nuestro ejemplo, que un explorador de Gopher encontró tres partes de mineral y un minero de Gopher logró extraer solo una parte de las reservas encontradas al mismo tiempo. Aquí, para que el reconocimiento de Gopher no pase la mayor parte de su tiempo, esperando que el minero termine su trabajo, usaremos canales almacenados. Comencemos creando un canal con una capacidad de 3.
bufferedChan := make(chan string, 3)
Podemos enviar varios datos al canal protegido, sin la necesidad de leerlos con otra rutina. Esta es la principal diferencia de los canales sin búfer.

bufferedChan := make(chan string, 3) go func() { bufferedChan <- "first" fmt.Println("Sent 1st") bufferedChan <- "second" fmt.Println("Sent 2nd") bufferedChan <- "third" fmt.Println("Sent 3rd") }() <-time.After(time.Second * 1) go func() { firstRead := <- bufferedChan fmt.Println("Receiving..") fmt.Println(firstRead) secondRead := <- bufferedChan fmt.Println(secondRead) thirdRead := <- bufferedChan fmt.Println(thirdRead) }()
El orden de salida en dicho programa será el siguiente:
Sent 1st Sent 2nd Sent 3rd Receiving.. first second third
Para evitar complicaciones innecesarias, no utilizaremos canales almacenados en nuestro programa. Pero es importante recordar que este tipo de canales también están disponibles para su uso.
También es importante tener en cuenta que los canales almacenados en búfer no siempre evitan el bloqueo. Por ejemplo, si un gopher scout es diez veces más rápido que un gopher miner, y están conectados a través de un canal protegido con una capacidad de 2, entonces el gopher scout se bloqueará cada vez que se envíe, si ya hay dos datos en el canal.
Poniendo todo junto
Entonces, armados con goroutines y canales, podemos escribir un programa usando todas las ventajas de la programación multiproceso en Go.

theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} oreChannel := make(chan string) minedOreChan := make(chan string)
Dicho programa generará lo siguiente:
From Finder: ore From Finder: ore From Miner: minedOre From Smelter: Ore is smelted From Miner: minedOre From Smelter: Ore is smelted From Finder: ore From Miner: minedOre From Smelter: Ore is smelted
En comparación con nuestro primer ejemplo, esta es una mejora importante, ahora todas las funciones se realizan de forma independiente, cada una en su propia rutina. Y también obtuvimos un transportador de canales, a través del cual el mineral se transfiere inmediatamente después del procesamiento. Para mantener el enfoque en una comprensión básica del funcionamiento de los canales y las rutinas, omití algunos puntos, que pueden generar dificultades para iniciar el programa. En conclusión, quiero hacer hincapié en estas características del lenguaje, ya que ayudan a trabajar con gorutinas y canales.
Gorutins anónimos

Así como ejecutamos una función regular en goroutine, podemos declarar una función anónima inmediatamente después de la palabra clave
go y llamarla usando la siguiente sintaxis:
Por lo tanto, si necesitamos llamar a una función en un solo lugar, podemos ejecutarla en una rutina diferente sin preocuparnos por su declaración por adelantado.
La función principal es la goroutina.

Sí, la función
principal funciona en su propia rutina. Y, lo que es más importante, después de su finalización, todas las otras gorutinas también terminan. Es por esta razón que colocamos una llamada de temporizador al final de nuestra función
principal . Esta llamada crea un canal y le envía datos después de 5 segundos.
<-time.After(time.Second * 5)
¿Recuerdas que goroutine se bloqueará cuando leas desde el canal hasta que se le envíe algo? Esto es exactamente lo que sucede cuando se agrega el código especificado. La gorutina principal se bloqueará, dando a las otras goroutias 5 segundos de tiempo para trabajar. Este método funciona bien, pero generalmente se utiliza un enfoque diferente para verificar que todas las gorutinas hayan completado su trabajo. Para transmitir una señal sobre la finalización del trabajo, se crea un canal especial, se bloquea la lectura de la gorutina principal y, tan pronto como la gorutina hija completa su trabajo, escribe en este canal; La gorutina principal se desbloquea y el programa finaliza.

func main() { doneChan := make(chan string) go func() {
Leer desde una tubería en un bucle de rango
En nuestro ejemplo, en la función del goffer-getter, utilizamos el bucle
for para seleccionar tres elementos del canal. Pero, ¿qué hacer si no se sabe de antemano cuántos datos puede haber en el canal? En tales casos, puede usar el canal como argumento para el bucle
for-range , al igual que con las colecciones. La función actualizada puede verse así:
Por lo tanto, el minero leerá todo lo que el explorador le envíe; usar el canal en el ciclo garantizará esto. Tenga en cuenta que después de que se hayan procesado todos los datos del canal, el ciclo se bloqueará en la lectura; para evitar el bloqueo, debe cerrar el canal llamando a
close (channel) .
Lectura de canal sin bloqueo
Usando la construcción
select-case , se pueden evitar las lecturas de bloqueo de la tubería. El siguiente es un ejemplo del uso de esta construcción: goroutine leerá los datos del canal, si solo está allí, de lo contrario se ejecutará el bloque
predeterminado :
myChan := make(chan string) go func(){ myChan <- “Message!” }() select { case msg := <- myChan: fmt.Println(msg) default: fmt.Println(“No Msg”) } <-time.After(time.Second * 1) select { case msg := <- myChan: fmt.Println(msg) default: fmt.Println(“No Msg”) }
Una vez iniciado, este código generará lo siguiente:
No Msg Message!
Grabación de canal sin bloqueo
Los bloqueos al escribir en un canal se pueden evitar mediante el uso de la misma construcción de
caso de selección . Hagamos una pequeña edición al ejemplo anterior:
select { case myChan <- “message”: fmt.Println(“sent the message”) default: fmt.Println(“no message sent”) }
Qué estudiar más

Hay una gran cantidad de artículos e informes que cubren el trabajo con canales y gorutinas con mucho más detalle. Y ahora, con el código tiene una idea clara de por qué y cómo se utilizan estas herramientas, puede aprovechar al máximo los siguientes materiales:
Gracias por tomarte el tiempo de leer. Espero haberte ayudado a comprender los canales, las rutinas y los beneficios que te brindan los programas multiproceso.