Go: Concurrencia y paralelismo

Golang ofrece herramientas para la concurrencia y el paralelismo. Descubre cómo utilizar goroutines, canales y otros mecanismos para aprovechar al máximo el procesamiento concurrente en Go.

Aprende Go GRATIS y certifícate

La concurrencia y el paralelismo son elementos fundamentales en la programación moderna, y Golang (Go) se destaca por ofrecer soporte nativo y eficiente para estos paradigmas. En este módulo, exploraremos cómo Go maneja la concurrencia y el paralelismo, y cómo puedes implementar estas características en tus proyectos para mejorar el rendimiento y la eficiencia.

Introducción a la concurrencia en Go

La concurrencia es la capacidad de gestionar múltiples tareas al mismo tiempo, permitiendo que un programa maneje varias operaciones de forma simultánea. En Go, la concurrencia es una característica central del lenguaje, facilitada a través de primitivas integradas como las goroutines y los canales.

Goroutines

Las goroutines son funciones o métodos que se ejecutan de manera concurrente con otras goroutines en el mismo espacio de direcciones. Son ligeras y gestionadas por el runtime de Go, lo que permite crear miles de goroutines sin un impacto significativo en los recursos del sistema.

Sintaxis básica

Para iniciar una goroutine, simplemente precede una llamada a función con la palabra clave go:

func main() {
    go sayHello()
    fmt.Println("Este mensaje puede aparecer antes que 'Hola'")
}

func sayHello() {
    fmt.Println("Hola")
}

En este ejemplo, sayHello() se ejecuta concurrentemente con main(). Dado que main() puede terminar antes de que sayHello() se ejecute, es posible que no veamos el mensaje "Hola" a menos que sincronizamos las goroutines.

Canales

Los canales en Go permiten la comunicación y sincronización entre goroutines. Actúan como conductos donde las goroutines pueden enviar y recibir valores de tipos específicos.

Creación de canales

Puedes crear un canal utilizando la función make:

ch := make(chan int)

Envío y recepción de datos

  • Envío: Utiliza el operador <- para enviar datos a un canal.
  ch <- valor
  • Recepción: También utilizas <- para recibir datos.
  valor := <-ch

Ejemplo de uso de canales

func main() {
    ch := make(chan string)
    go greet(ch)
    message := <-ch
    fmt.Println(message)
}

func greet(ch chan string) {
    ch <- "Hola desde la goroutine"
}

En este ejemplo, la función greet envía un mensaje a través del canal ch, y main lo recibe y lo imprime.

Paralelismo en Go

El paralelismo implica ejecutar múltiples operaciones al mismo tiempo en sistemas con múltiples núcleos o procesadores. Mientras que la concurrencia es sobre gestionar muchas tareas a la vez, el paralelismo es sobre ejecutarlas simultáneamente.

Configuración del paralelismo

Go permite especificar el número de núcleos utilizados a través de la función runtime.GOMAXPROCS. Por defecto, Go utiliza todos los núcleos disponibles.

import "runtime"

func main() {
    runtime.GOMAXPROCS(4) // Utilizar 4 núcleos
    // Código concurrente aquí
}

Patrones comunes de concurrencia

Select

La sentencia select permite esperar en múltiples operaciones de envío o recepción en canales.

Ejemplo de select

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        ch1 <- "Mensaje del canal 1"
    }()

    go func() {
        ch2 <- "Mensaje del canal 2"
    }()

    select {
    case msg1 := <-ch1:
        fmt.Println(msg1)
    case msg2 := <-ch2:
        fmt.Println(msg2)
    }
}

En este ejemplo, select espera a que uno de los canales reciba un mensaje y luego ejecuta el caso correspondiente.

WaitGroup

El paquete sync ofrece el tipo WaitGroup para esperar a que un conjunto de goroutines terminen su ejecución.

Uso de WaitGroup

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        fmt.Println("Goroutine 1 terminada")
    }()

    go func() {
        defer wg.Done()
        fmt.Println("Goroutine 2 terminada")
    }()

    wg.Wait()
    fmt.Println("Todas las goroutines han terminado")
}

En este ejemplo, WaitGroup espera a que las dos goroutines llamen a Done antes de continuar.

Contextos en Go

El paquete context proporciona funciones para controlar la vida de las goroutines. Útil para operaciones cancelables y con tiempo límite.

Creación de un contexto con cancelación

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    go func() {
        // Simular trabajo
        time.Sleep(2 * time.Second)
        cancel()
    }()

    select {
    case <-ctx.Done():
        fmt.Println("Operación cancelada")
    }
}

Prácticas recomendadas

  • Evitar bloqueos: Asegúrate de que las goroutines no quedan bloqueadas esperando en canales sin receptores.

  • Cerrar canales correctamente: Cuando ya no necesites un canal, ciérralo para evitar fugas de recursos.

  close(ch)
  • Uso de mutexes: Para secciones críticas donde el acceso concurrente puede causar condiciones de carrera, utiliza sync.Mutex.
  var mu sync.Mutex

  mu.Lock()
  // Sección crítica
  mu.Unlock()

Ejemplo completo: procesamiento concurrente

A continuación, un ejemplo que demuestra el uso de goroutines y canales para procesar datos concurrentemente.

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for j := range jobs {
        fmt.Printf("Trabajador %d procesando trabajo %d\n", id, j)
        time.Sleep(time.Second * time.Duration(rand.Intn(3)))
        results <- j * 2
    }
}

func main() {
    rand.Seed(time.Now().Unix())

    jobs := make(chan int, 100)
    results := make(chan int, 100)

    var wg sync.WaitGroup

    // Iniciar trabajadores
    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // Enviar trabajos
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    // Esperar a que los trabajadores terminen
    wg.Wait()
    close(results)

    // Recoger resultados
    for result := range results {
        fmt.Printf("Resultado: %d\n", result)
    }
}

Este programa crea tres trabajadores que procesan cinco trabajos. Cada trabajador procesa un trabajo y envía el resultado al canal results.

Conclusión

La concurrencia y el paralelismo en Golang permiten desarrollar aplicaciones eficientes y escalables. Al dominar goroutines, canales y otras herramientas concurrentes, puedes aprovechar al máximo los recursos del sistema y mejorar el rendimiento de tus aplicaciones.

Empezar curso de Go

Lecciones de este módulo de Go

Lecciones de programación del módulo Concurrencia y paralelismo del curso de Go.

Ejercicios de programación en este módulo de Go

Evalúa tus conocimientos en Concurrencia y paralelismo con ejercicios de programación Concurrencia y paralelismo de tipo Test, Puzzle, Código y Proyecto con VSCode.