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:
Veamos un ejemplo sobre como podemos usar goroutines en golang:
go func() {
// Código que se ejecutará en una goroutine
}()
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
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)
}
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
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
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
¿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