sync en Go: Mutex, RWMutex, WaitGroup, Once, Pool y Map para concurrencia segura

Cuando varias goroutines acceden a datos compartidos y al menos una escribe, necesitas sincronización explícita. El paquete sync de Go ofrece las primitivas fundamentales: Mutex para acceso exclusivo, RWMutex para lectura concurrente y escritura exclusiva, WaitGroup para esperar goroutines, Once para inicialización única, Pool para reutilizar objetos y sync.Map para mapas concurrentes.

sync.Mutex: acceso exclusivo

package main

import (
    "fmt"
    "sync"
)

type Contador struct {
    mu    sync.Mutex
    valor int
}

func (c *Contador) Incrementar() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.valor++
}

func (c *Contador) Valor() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.valor
}

func main() {
    c := &Contador{}
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Incrementar()
        }()
    }

    wg.Wait()
    fmt.Println(c.Valor()) // siempre 1000
}

Nunca copies un Mutex por valor: las goroutines que usen la copia tendrán un mutex distinto y la sincronización no funcionará. Pasa siempre un puntero a la estructura que lo contiene.

sync.RWMutex: lecturas concurrentes

RWMutex permite que múltiples lectores accedan simultáneamente, pero la escritura es exclusiva. Mejora el rendimiento en caches y registros donde las lecturas dominan:

type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(clave string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.data[clave]
    return v, ok
}

func (c *Cache) Set(clave, valor string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[clave] = valor
}

sync.WaitGroup: esperar a un grupo de goroutines

WaitGroup lleva la cuenta de goroutines activas. Add(n) incrementa el contador, Done() lo decrementa y Wait() bloquea hasta llegar a cero:

func procesarLote(urls []string) {
    var wg sync.WaitGroup
    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            fmt.Println("descargando:", u)
        }(url)
    }
    wg.Wait()
    fmt.Println("todas las descargas completadas")
}

sync.Once: inicialización garantizada

Once.Do ejecuta la función exactamente una vez aunque la llamen múltiples goroutines en paralelo. Ideal para el patrón singleton:

var (
    instanciaBD *BD
    once        sync.Once
)

func ObtenerBD() *BD {
    once.Do(func() {
        instanciaBD = &BD{}
        instanciaBD.Conectar("localhost:5432")
    })
    return instanciaBD
}

sync.Pool: reutilizar objetos costosos

Pool mantiene un conjunto de objetos reutilizables para reducir la presión sobre el GC. Muy útil para buffers y estructuras de uso frecuente:

var bufPool = sync.Pool{
    New: func() any {
        return make([]byte, 4096)
    },
}

func handler(datos []byte) {
    buf := bufPool.Get().([]byte)
    defer bufPool.Put(buf)

    // usar buf...
    copy(buf, datos)
    fmt.Printf("procesados %d bytesn", len(datos))
}

El GC puede vaciar el pool en cualquier momento, así que úsalo solo para mejorar el rendimiento, no para garantizar persistencia de objetos.

sync.Map: mapa concurrente

sync.Map es un mapa seguro para acceso concurrente sin necesidad de un mutex externo. Está optimizado para patrones de escritura-una-vez y lectura-frecuente:

var m sync.Map

// Store y Load
m.Store("clave", "valor")
if v, ok := m.Load("clave"); ok {
    fmt.Println(v.(string))
}

// LoadOrStore: carga si existe, guarda si no
actual, loaded := m.LoadOrStore("clave", "nuevo")
fmt.Println(actual, loaded) // "valor", true

// Range: iterar sobre todos los pares
m.Range(func(k, v any) bool {
    fmt.Printf("%s ? %sn", k, v)
    return true // devuelve false para parar la iteración
})

Para la mayoría de casos, un map normal con sync.RWMutex es más eficiente y más fácil de razonar que sync.Map. Usa sync.Map cuando tengas muchas goroutines leyendo claves distintas que raramente se solapan.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP