GO: Concurrencia vs Paralelismo Para Tontos.

Lucatonny Raudales - Aug 23 - - Dev Community

Bienvenido a este post con un título un poco denigrante.
Pero, en este post quiero explicarte que son estas 2 características de la programación de una forma muy sencilla, en esta ocasión usando mi lenguaje de programación favorito GOLANG.

Imaginemos una cocina:

Cocinar un plato: Esto representa una tarea.
Un cocinero: Es un procesador.
Concurrencia:

Varios cocineros en la cocina: Cada uno preparando un plato diferente.
En Go: Cada cocinero sería una goroutine. Aunque la cocina (el procesador) solo tiene un horno, los cocineros pueden trabajar en sus platos de forma simultánea, pasando el tiempo en otras tareas mientras esperan que el horno esté disponible.
Paralelismo:

Varios hornos: Cada cocinero tiene su propio horno.
En Go: Si tenemos múltiples procesadores físicos, cada goroutine podría ejecutarse en un procesador diferente, cocinando varios platos al mismo tiempo de manera real.

¿Cuál es la diferencia?

Concurrencia: Las tareas se ejecutan de forma entrelazada, dando la ilusión de paralelismo, incluso en un solo procesador.
Paralelismo: Las tareas se ejecutan simultáneamente en múltiples procesadores, lo que acelera significativamente el proceso.

¿Cómo usarlos en Go?

Goroutines: Son como hilos ligeros. Para crear una goroutine, simplemente utilizamos la palabra clave go antes de una función:

Go Routines

Veamos un ejemplo sobre como podemos usar goroutines en golang:

go func() {
    // Código que se ejecutará en una goroutine
}()
Enter fullscreen mode Exit fullscreen mode

Canales: Son tuberías por las cuales las goroutines pueden comunicarse y sincronizarse.
Imagina que son tubos para pasar ingredientes entre los cocineros

ch := make(chan int)
go func() {
    ch <- 42 // Enviar un valor por el canal
}()
value := <-ch // Recibir un valor del canal
Enter fullscreen mode Exit fullscreen mode

Ejemplo práctico:

package main

import (
    "fmt"
    "time"
)

func worker(id int, c chan int) {
    for n := range c {
        fmt.Printf("Worker %d received %d\n", id, n)
        time.Sleep(time.Second)
    }
}

func main() {
    c := make(chan int)

    for i := 1; i <= 5; i++ {
        go worker(i, c)
    }

    for n := 1; n <= 10; n++ {
        c <- n
    }
    close(c)

    time.Sleep(time.Second)
}
Enter fullscreen mode Exit fullscreen mode

La salida de este código sería

Worker 1 received 1
Worker 2 received 2
Worker 3 received 3
Worker 4 received 4
Worker 5 received 5
Worker 1 received 6
Worker 2 received 7
Worker 3 received 8
Worker 4 received 9
Worker 5 received 10
Enter fullscreen mode Exit fullscreen mode

aunque aveces podría verse así

Worker 5 received 1
Worker 1 received 3
Worker 2 received 2
Worker 4 received 5
Worker 3 received 4
Worker 3 received 6
Worker 5 received 10
Worker 2 received 8
Worker 4 received 7
Worker 1 received 9
Enter fullscreen mode Exit fullscreen mode

o así

Worker 5 received 1
Worker 1 received 2
Worker 2 received 3
Worker 3 received 4
Worker 4 received 5
Worker 1 received 6
Worker 2 received 7
Worker 3 received 8
Worker 5 received 9
Worker 4 received 10
Enter fullscreen mode Exit fullscreen mode

¿Por qué la salida cambia cada vez que ejecuto el programa?

La razón principal por la que la salida del programa cambia en cada ejecución es debido a la naturaleza no determinista de la concurrencia.

Aquí hay un desglose de lo que está sucediendo:

Crear un canal: make(chan int) crea un canal de enteros. Este canal se utilizará para la comunicación entre las goroutines.

Iniciar goroutines: El bucle for i := 1; i <= 5; i++ { go worker(i, c) } inicia cinco goroutines, cada una con un ID único.
La función worker recibe el ID y el canal.

Enviar valores al canal: El bucle for n := 1; n <= 10; n++ { c <- n } envía los valores del 1 al 10 al canal.

Cerrar el canal: La llamada close(c) cierra el canal, indicando que no se enviarán más valores.

Recibir valores del canal: Cada goroutine recibe valores del canal usando el bucle for n := range c. Cuando se recibe un valor, se imprime en la consola.

Esperar a que las goroutines terminen: La llamada time.Sleep(time.Second) asegura que la goroutine principal espere a que las otras goroutines terminen antes de salir.

Hasta ahora:

Creamos 5 goroutines (cocineros) que reciben números por un canal.
Enviamos números al canal para que los cocineros los procesen.
Los cocineros trabajan de forma concurrente, procesando los números a medida que los reciben.

¿Por qué usar concurrencia y paralelismo en Go?

Mejor rendimiento: Especialmente en tareas I/O-bound (como leer archivos o hacer solicitudes HTTP).
Mayor capacidad de respuesta: La aplicación puede seguir respondiendo a otras solicitudes mientras una tarea está bloqueada.
Arquitecturas más escalables: Puedes distribuir el trabajo en múltiples núcleos o máquinas.

¡Recuerda!

La concurrencia y el paralelismo son herramientas poderosas, pero también pueden hacer que el código sea más complejo de entender y depurar. Es importante usarlas con cuidado y comprender sus implicaciones.

¿Quieres profundizar más en algún tema específico?

Podemos explorar conceptos como:

Sincronización: Mutexes, grupos de trabajo, etc.
Patrones de concurrencia: Producer-consumer, pipeline, etc.
Testing concurrente: Cómo probar código concurrente de manera efectiva.

Saludos,
Lucatonny Raudales

X/Twitter
Github

. . . . .