Implementación de patrones concurrentes y sincronización en Go
En Go, hay varios patrones de programación concurrente y herramientas de sincronización que pueden ayudarte a escribir programas concurrentes eficientes y confiables. Exploremos algunos de los conceptos clave y cómo implementarlos.
Patrones concurrentes
Pools de trabajadores (Worker Pools)
Uno de los patrones concurrentes comunes en Go es el pool de trabajadores. En este patrón, se crea un grupo de goroutines trabajadores que pueden procesar tareas de forma concurrente. Esto puede ser útil para tareas que se pueden paralelizar, como procesar un gran conjunto de datos o ejecutar solicitudes de red independientes.
A continuación, se muestra un ejemplo de una implementación simple de un pool de trabajadores en Go:
package main
import (
"fmt"
"sync"
)
func main() {
const numWorkers = 4
const numJobs = 10
var wg sync.WaitGroup
jobs := make(chan int, numJobs)
// Start worker goroutines
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", i, job)
}
}()
}
// Send jobs to the worker pool
for i := 0; i < numJobs; i++ {
jobs <- i
}
close(jobs)
wg.Wait()
}
En este ejemplo, creamos un canal para almacenar las tareas y un grupo de 4 goroutines trabajadores que extraen las tareas del canal y las procesan. El sync.WaitGroup
se utiliza para garantizar que todos los trabajadores hayan terminado antes de que el programa finalice.
Pipelines
Otro patrón concurrente común en Go es el patrón de pipeline. En este patrón, se crea una serie de etapas, donde cada etapa procesa los datos y los pasa a la siguiente etapa. Esto puede ser útil para procesar datos en una secuencia de pasos, como obtener datos, transformarlos y luego almacenarlos.
A continuación, se muestra un ejemplo de un pipeline simple en Go:
package main
import "fmt"
func main() {
// Create the pipeline stages
numbers := generateNumbers(10)
squares := squareNumbers(numbers)
results := printResults(squares)
// Run the pipeline
for result := range results {
fmt.Println(result)
}
}
func generateNumbers(n int) <-chan int {
out := make(chan int)
go func() {
for i := 0; i < n; i++ {
out <- i
}
close(out)
}()
return out
}
func squareNumbers(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for num := range in {
out <- num * num
}
close(out)
}()
return out
}
func printResults(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for num := range in {
out <- num
}
close(out)
}()
return out
}
En este ejemplo, creamos tres etapas del pipeline: generateNumbers
, squareNumbers
y printResults
. Cada etapa es una función que lee de un canal de entrada, procesa los datos y escribe los resultados en un canal de salida.
Herramientas de sincronización
Go proporciona varias primitivas de sincronización que pueden ayudarte a coordinar el acceso concurrente a recursos compartidos y evitar condiciones de carrera (race conditions).
Mutexes
El tipo sync.Mutex
es un bloqueo de exclusión mutua que te permite proteger los recursos compartidos del acceso concurrente. Solo una goroutine puede mantener el bloqueo a la vez, lo que garantiza que las secciones críticas de tu código se ejecuten de forma atómica.
var counter int
var mutex sync.Mutex
func incrementCounter() {
mutex.Lock()
defer mutex.Unlock()
counter++
}
WaitGroups
El tipo sync.WaitGroup
te permite esperar a que un conjunto de goroutines termine antes de continuar. Esto es útil para coordinar la ejecución de múltiples goroutines.
var wg sync.WaitGroup
func doWork() {
defer wg.Done()
// Do some work
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
go doWork()
}
wg.Wait()
// All goroutines have finished
}
Canales (Channels)
Los canales en Go son una herramienta poderosa para comunicar entre goroutines. Se pueden utilizar para pasar datos, señales y primitivas de sincronización entre procesos concurrentes.
func producer(out chan<- int) {
out <- 42
close(out)
}
func consumer(in <-chan int) {
num := <-in
fmt.Println("Received:", num)
}
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}
Al combinar estos patrones concurrentes y herramientas de sincronización, puedes escribir programas concurrentes de Go eficientes y confiables que gestionen eficazmente los recursos compartidos y eviten las condiciones de carrera.