Concurrencia y multihilo en Go: nivel avanzado
\n\nLa concurrencia (concurrency) es una de las características clave del lenguaje Go, integrada en su arquitectura a nivel de runtime y sintaxis. A diferencia de muchos otros lenguajes, donde el multihilo se implementa mediante bibliotecas complejas y requiere gestión manual de hilos, Go ofrece gorutinas (goroutines) ligeras y canales (channels). A nivel avanzado, no basta con ejecutar varias tareas, sino que es necesario sincronizarlas correctamente, evitar condiciones de carrera y utilizar eficientemente los recursos del sistema. En este artículo, analizaremos técnicas avanzadas para trabajar con concurrencia en Go.
\n\n1. Gorutinas: gestión del ciclo de vida y prioridades
\n\nLas gorutinas son hilos ligeros gestionados por el runtime de Go. Se inician con la palabra clave go. Sin embargo, a nivel avanzado es necesario comprender cómo controlar su ejecución y finalización.
Inicio básico de una gorutina:
\npackage main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc worker(id int) {\n fmt.Printf("Worker %d starting\\n", id)\n time.Sleep(time.Second)\n fmt.Printf("Worker %d done\\n", id)\n}\n\nfunc main() {\n for i := 1; i <= 5; i++ {\n go worker(i)\n }\n time.Sleep(2 * time.Second) // Muleta: esperamos a que termine\n}\n\nProblema: en este ejemplo usamos time.Sleep para esperar, lo cual no es fiable. A nivel avanzado se utilizan canales y sync.WaitGroup.
2. Sincronización con sync.WaitGroup
\n\nsync.WaitGroup permite esperar a que finalice un número determinado de gorutinas. Es una herramienta básica pero potente para la sincronización.
package main\n\nimport (\n "fmt"\n "sync"\n "time"\n)\n\nfunc worker(id int, wg *sync.WaitGroup) {\n defer wg.Done() // Disminuimos el contador al finalizar\n fmt.Printf("Worker %d starting\\n", id)\n time.Sleep(time.Second)\n fmt.Printf("Worker %d done\\n", id)\n}\n\nfunc main() {\n var wg sync.WaitGroup\n for i := 1; i <= 5; i++ {\n wg.Add(1) // Aumentamos el contador antes de iniciar\n go worker(i, &wg)\n }\n wg.Wait() // Bloqueamos hasta que el contador llegue a cero\n fmt.Println("All workers completed")\n}\n\nImportante: wg.Add() debe llamarse antes de iniciar la gorutina, de lo contrario podría haber una condición de carrera. Use defer wg.Done() dentro de la gorutina para garantizar la disminución del contador incluso en caso de pánico.
3. Canales: buffering, dirección y select
\n\nLos canales son el mecanismo principal para intercambiar datos entre gorutinas. A nivel avanzado se utilizan canales con buffer, canales unidireccionales y el operador select.
3.1. Canales con buffer
\nLos canales con buffer permiten enviar datos sin necesidad de recibirlos inmediatamente. Esto reduce los bloqueos y aumenta el rendimiento.
\npackage main\n\nimport "fmt"\n\nfunc main() {\n ch := make(chan int, 3) // canal con buffer de 3 elementos\n ch <- 1\n ch <- 2\n ch <- 3\n // ch <- 4 // bloqueo, ya que el buffer está lleno\n fmt.Println(<-ch)\n fmt.Println(<-ch)\n fmt.Println(<-ch)\n}\n\n3.2. Canales unidireccionales
\nPara la seguridad de tipos y para indicar explícitamente el rol del canal, se utilizan canales unidireccionales: chan<- (solo envío) y <-chan (solo recepción).
package main\n\nimport "fmt"\n\nfunc sender(out chan<- int) {\n for i := 0; i < 5; i++ {\n out <- i\n }\n close(out)\n}\n\nfunc receiver(in <-chan int) {\n for val := range in {\n fmt.Println(val)\n }\n}\n\nfunc main() {\n ch := make(chan int)\n go sender(ch)\n receiver(ch)\n}\n\n3.3. Operador select
\nselect permite que una gorutina espere en múltiples canales simultáneamente. Esto