Mostrando entradas con la etiqueta Avanzado. Mostrar todas las entradas
Mostrando entradas con la etiqueta Avanzado. Mostrar todas las entradas

viernes, 7 de diciembre de 2018

Punteros en funciones en Go

Cuando trabajamos con funciones, los parámetros habituales reciben valores. En sí, los parámetros son variables definidas dentro del bloque de código de la función, por lo que su visibilidad y ámbito se refieren exclusivamente a la función (ver capítulo "Visibilidad de datos y funciones en go").

En el siguiente ejemplo, definimos la variable contador, con el valor 1. A continuación, llama la función incrementar(), enviando el valor de la variable contador. Esta función, recibe el valor y lo asigna al parámetro o variable valor. Ambas variables son diferentes, están definidas en diferentes bloques de código, apuntan a diferentes direcciones de memoria. La función, incrementa el valor de la variable valor, pero no afecta ni puede hacer nada con la variable contador.

package main

import "fmt"

func main() {
   var contador int = 1

   // llama a la funcion pasando el valor
   incrementar(contador) // 1
   fmt.Println("contador = ", contador)
}

// El parametro valor recibe el valor de contador
// valor apunta a una direccion de memoria distinta
// de contador. Por tanto, ambas variables
// apuntan a diferentes direcciones y son diferentes
func incrementar(valor int) {
   valor++ // Se asigna a la memoria de valor
}

En el siguiente ejemplo vamos a usar punteros:

package main

import "fmt"

func main() {
   var contador int = 1

   // llama a la funcion pasando la direccion
   // de memoria de la variable contador
   incr(&contador)
   fmt.Println("contador = ", contador) // 2
}

// El parametro valor es un puntero que recibe
// la direccion de memoria de la variable contador
func incr(valor *int) {
   // valor apunta a &contador
   // *valor asigna el valor a la direccion memoria
   *valor++ // Se asigna a la memoria de contador
}

En este caso, el parámetro de la función es un puntero, y recibe una dirección de memoria a la que referenciar. Cuando se llama a la función, se pasa la indirección de la variable contador. Por tanto, si el parámetro apunta a la dirección de memoria de la variable contador, las operaciones del puntero afectarán a dicha dirección de memoria, y, por tanto, también a la variable contador. En la función utilizamos el puntero para incrementar el valor actual, lo que se ve reflejado también en la variable contador.

Enlaces de interés

Punteros en Go

El concepto de punteros es heredado del lenguaje C, y es muy útil para determinados escenarios, tales como las funciones o las estructuras.

Una variable es un nombre que utilizamos para referirnos a un valor o dato. Dicho valor o dato se almacena en alguna posición de la memoria del ordenador.

Podemos conocer la dirección de memoria de una variable utilizando el operador de indirección & (ampersand):

var x int = 10
fmt.Println("x=", x)   // 10
fmt.Println("&x=", &x) // 0xc0000120b0

Un puntero es, básicamente, una variable que apunta a la dirección de memoria de otra variable. Por tanto, mediante un puntero podemos operar directamente sobre esa dirección de memoria y, por consiguiente, sobre el valor de la primera variable.

package main

import "fmt"

func main() {

   var x int = 10

   fmt.Println("x=", x)   // 10
   fmt.Println("&x apunta a ", &x) // 0xc0000120b0 

   // ptrx es un puntero que apunta a
   // la direccion de memoria de x
   ptrx := &x

   // Almacena en la direccion de memoria a
   // la que apunta ptrx el valor 15
   *ptrx = 15 // Idem a x=15

   fmt.Println("x=", x)   // 15

   fmt.Println("ptrx apunta a ", ptrx) // 0xc0000120b0
   fmt.Println("*ptrx vale ", *ptrx)   // 15
   fmt.Println("&x apunta a ", &x)     // 0xc0000120b0
}

El operador puntero * (asterisco), hace referencia a una dirección de memoria. Cuando se utiliza para asignar un valor, este valor es asignado a la dirección de memoria a la que apunta el puntero, que es la misma dirección de memoria que la variable direccionada mediante el operador de indirección. Es decir, no se asigna el valor a la variable, si no a la dirección de memoria.

Enlaces de interés

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