lunes, 5 de noviembre de 2018

Concurrencia y paralelismo en Go: conceptos

Go puede realizar varias cosas al mismo tiempo. Esto significa que podemos lanzar un proceso y, mientras éste se está ejecutando, realizar otras cosas, sin tener que esperar a que el proceso termine. Esto permite optimizar mucho los tiempos de ejecución y el rendimiento de nuestras aplicaciones.

Concurrencia

Cuando el trabajo lo realiza una única CPU, podemos dividir el tiempo de procesamiento de esta CPU de forma que los diferentes procesos parezcan que se ejecutan de forma concurrente, cuando en realidad está haciendo una cosa cada vez. En este caso, la CPU está priorizando tiempos de ejecución de cada proceso o tarea, intercalando una fracción mínima de tiempo a cada proceso. La palabra clave en este caso, es la coordinación de los tiempos de ejecución de cada tarea.

Paralelismo

Los ordenadores de hoy en día tienen procesadores multi-core, por lo que disponen de varias CPUs (Unidades Centrales de Procesamiento), capaces de ejecutar procesos diferentes en paralelo y al mismo tiempo.

Diferencia entre concurrencia y paralelismo

La principal diferencia entre concurrencia y paralelismo es que la concurrencia está sincronizando o coordinando varias cosas a la vez, mientras que el paralelismo está haciendo varias cosas a la vez.

Hilos de ejecución

Todo programa comienza con un hilo de ejecución (hilo principal), y cuando este hilo principal acaba, el programa finaliza. El hilo ejecuta un proceso, el cual contiene código y datos en la memoria.

Dentro de un proceso podemos abrir nuevos hilos de ejecución, cada uno de los cuales ejecutará un proceso (o fragmento de código). Cada hilo tiene acceso a una zona de memoria determinada, y las variables creadas por cada hilo son exclusivas (no se comparten) para dicho proceso a través de un stack o pila. Por otra parte, también disponemos de un heap (o montón), que es una zona compartida de memoria que podemos compartir datos entre procesos.

Una aplicación con múltiples hilos de ejecución (multi-thread) permite ejecutar procesos en serie (concurrencia) o en paralelo.

gorutinas

Una gorutina (goroutine), es una función que se ejecuta de forma concurrente o en paralelo. A diferencia de un hilo estándar (o thread), una gorutina posee las siguientes características:

  • Es creada, ejecutada, sincronizada y destruida automáticamente por Go a través de su runtime (o controlador de ejecución).
  • Es gestionada por Go, no por el sistema.

  • No tiene dependencias del hardware.

  • Puede dimensionar dinámicamente el tamaño de su stack para almacenar datos en tiempo de ejecución.
  • Puede usar canales de comunicación con otras gorutinas con latencia baja.
  • Puede trabajar de forma sincronizada y colaborativa con otras gorutinas.

Básicamente, una gorutina puede ser cualquier función que definamos en nuestras aplicaciones. La diferencia radica en la forma en que se invoca o ejecuta, a través de la palabra clave go, la cual permite al controlador de ejecución de Go tomar el control de dicha función para que se ejecute de forma asíncrona.

go función(argumentos)

Ejemplo

El siguiente código de ejemplo ilustra, de forma sencilla, cómo funciona la ejecución concurrente (o asíncrona) mediante una gorutina.

package main

import (
   "fmt"
   "time"
)

func main() {
   // Ejecucion sincrona
   repetir(3, "Síncrono inicio")

   // Ejecucion asincrona
   go repetir(5, "Asíncrono")

   // Nueva ejecucion sincrona
   repetir(3, "Síncrono final")

   // Esperar a que el usuario pulse tecla Enter
   fmt.Scanln()
   fmt.Println("Fin del programa")
}

func repetir(veces int, texto string) {
   for i := 1; i <= veces; i++ {
      fmt.Printf("#%v > %v\n", i, texto)
      time.Sleep(10 * time.Millisecond)
   }
}

Hemos definido una función llamada repetir(), la cual se encarga de imprimir n veces un texto pasado como argumento. El bucle for se encargará de este trabajo, y entre cada iteración o impresión, realizamos un retardo de diez milisegundos. Ello se consigue mediante el paquete time, el cual incluye funciones muy útiles relacionadas con la gestión del tiempo. Entre ellas, encontramos la función Sleep(), la cual permite esperar el tiempo que le indiquemos.

Dentro de la función main(), invocamos a esta función de forma normal (síncrona). Esto es, se ejecuta paso a paso hasta que termine. Por este motivo, hasta que no termine su ejecución no continuará su ejecución.

Después, se invocará nuevamente mediante la palabra clave go, con lo cual, el controlador de ejecución de Go tomará esta ejecución como un hilo concurrente y lo ejecutará de forma asíncrona. Es decir, que este proceso se ejecutará en segundo plano sin esperar a que termine la ejecución, continuando la ejecución la siguiente línea de código de la función main().

A continuación se ejecutará nuevamente de forma síncrona (normal), pero, al mismo tiempo, el proceso anterior se seguirá ejecutando.

Una vez terminada la ejecución anterior (es decir, se ha impreso 3 veces el texto "Síncrono final), esperamos a que el usuario pulse la tecla Enter, mediante la función Scanln del paquete de entrada y salida fmt. Después, imprime el mensaje "Fin del programa" y finaliza la ejecución del hilo principal (función main().

El resultado será el siguiente:

$ go run concurrency-concept.go

#1 > Síncrono inicio
#2 > Síncrono inicio
#3 > Síncrono inicio
#1 > Síncrono final
#1 > Asíncrono
#2 > Asíncrono
#2 > Síncrono final
#3 > Síncrono final
#3 > Asíncrono
#4 > Asíncrono
#5 > Asíncrono

Fin del programa

Ejemplo 2

Para el siguiente ejemplo, simplemente comentaremos la línea en que el programa espera a que el usuario pulse la tecla Enter:

   // fmt.Scanln()

Ahora el resultado será el siguiente:

$ go run concurrency-concept.go
#1 > Síncrono inicio
#2 > Síncrono inicio
#3 > Síncrono inicio
#1 > Síncrono final
#1 > Asíncrono
#2 > Asíncrono
#2 > Síncrono final
#3 > Asíncrono
#3 > Síncrono final
Fin del programa

Aquí se puede apreciar que, una vez ha terminado la ejecución síncrona del texto "Síncrono final", se ha lanzado la impresión de "Fin del programa" y el programa finaliza, mientras aún quedaban iteraciones de la gorutina. El proceso main() es el hilo principal, el cual lanzó un hilo concurrente que aún no terminó cuando llegó al final de la ejecución. Por tanto, el hilo principal es el que manda, y, una vez finaliza éste, se corta y finalizan todos los sub-hilos en curso.

Enlaces de interés

2 comentarios:

  1. Ésto me genera una duda que tal vez no estoy viendo...
    Si una gorutina puede ser concurrente o paralela ¿Cómo las diferencio a la hora de escribir un programa?

    ResponderEliminar
    Respuestas
    1. En primer lugar, no escribimos código paralelo, solo el código concurrente que deseamos ejecutar en paralelo.
      Además, el paralelismo es un atributo de nuestro programa en tiempo de ejecución, no un atributo de nuestro código.

      https://programmerclick.com/article/27501158021/

      Eliminar