sync.Mutex y RWMutex en Go: proteger datos compartidos entre goroutines

Cuando varias goroutines acceden a datos compartidos y al menos una escribe, necesitas sincronización. El paquete sync de Go ofrece las herramientas fundamentales: Mutex para acceso exclusivo, RWMutex para permitir lecturas simultáneas, Once para inicialización única y sync/atomic para operaciones atómicas de bajo nivel.

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() // siempre desbloquear, incluso si hay un pánico
    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
}

El defer c.mu.Unlock() después del Lock garantiza que el mutex se libera incluso si el código posterior entra en pánico.

Deadlock: el error más común con mutexes

Un deadlock ocurre cuando una goroutine espera un mutex que ella misma ya tiene bloqueado, o cuando dos goroutines esperan mutuamente al mutex de la otra. Go lo detecta y termina el programa:

// PELIGRO: Lock dentro de Lock sin Unlock intermedio
func (c *Contador) Doble() {
    c.mu.Lock()
    c.Incrementar() // intenta Lock de nuevo ? deadlock
    c.mu.Unlock()
}

sync.RWMutex: lectores simultáneos

RWMutex permite que múltiples goroutines lean al mismo tiempo pero solo una escriba. Mejora el rendimiento en cargas de trabajo con muchas lecturas y pocas escrituras:

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

func (c *Cache) Get(clave string) (string, bool) {
    c.mu.RLock()         // bloqueo de lectura: otros también pueden leer
    defer c.mu.RUnlock()
    v, ok := c.data[clave]
    return v, ok
}

func (c *Cache) Set(clave, valor string) {
    c.mu.Lock()          // bloqueo exclusivo: nadie más puede leer ni escribir
    defer c.mu.Unlock()
    c.data[clave] = valor
}

sync.Once: inicialización thread-safe

sync.Once garantiza que una función se ejecuta exactamente una vez, aunque múltiples goroutines la llamen simultáneamente. Es la forma idiomática de implementar el patrón singleton:

var (
    instancia *BD
    once      sync.Once
)

func ObtenerBD() *BD {
    once.Do(func() {
        instancia = &BD{} // se ejecuta solo la primera vez
        instancia.Conectar("localhost:5432")
    })
    return instancia
}

sync/atomic: operaciones atómicas

Para contadores simples, las operaciones atómicas son más eficientes que un mutex porque operan a nivel de instrucción de CPU sin bloquear:

import "sync/atomic"

type ContadorAtomico struct {
    valor int64
}

func (c *ContadorAtomico) Incrementar() {
    atomic.AddInt64(&c.valor, 1)
}

func (c *ContadorAtomico) Valor() int64 {
    return atomic.LoadInt64(&c.valor)
}

// Desde Go 1.19: tipos atómicos genéricos
var bandera atomic.Bool
bandera.Store(true)
fmt.Println(bandera.Load()) // true

Cuándo usar cada mecanismo

  • sync.Mutex — cuando varias goroutines necesitan acceso exclusivo a una estructura de datos compleja.
  • sync.RWMutex — cuando hay muchas más lecturas que escrituras (caches, registros).
  • sync.Once — para inicialización perezosa segura entre goroutines.
  • sync/atomic — para contadores simples y flags booleanos sin necesidad de proteger una estructura compleja.
  • Canales — cuando necesitas comunicar datos entre goroutines, no solo sincronizar el acceso.

COMPARTE ESTE ARTÍCULO

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