Proteger datos compartidos
Un sync.Mutex deja entrar a una sola goroutine a la vez en el código entre Lock y Unlock. Pon el mutex junto a los datos que protege, normalmente en el mismo struct:
Esto siempre imprime hits: 10000. Sin el lock, 100 goroutines escribiendo en el mismo map suelen hacer caer el programa con fatal error: concurrent map writes. Ese error viene de una comprobación del runtime que hace lo que puede, no se puede recuperar, y que no haya caída no demuestra que el código sea correcto.
Detalles que importan:
- El valor cero de
sync.Mutexestá desbloqueado y listo. Sin constructor. - Los métodos usan un receptor puntero (
*Counter). Un receptor por valor bloquearía una copia del mutex, que no protege nada. defer c.mu.Unlock()justo después deLockhace que cada ruta de retorno, y también un panic, liberen el lock.- Todo acceso pasa por el lock, lecturas incluidas. Una lectura sin lock mientras otra goroutine escribe sigue siendo una carrera de datos.
Mantén pequeña la sección crítica
defer desbloquea al final de la función. Es lo correcto para métodos cortos como los de arriba. En una función más larga, desbloquea en cuanto dejes de tocar los datos compartidos, para que otras goroutines no esperen por trabajo que no necesita el lock:
func (s *Store) Save(key string) error {
s.mu.Lock()
data := s.items[key] // copy what you need
s.mu.Unlock()
return writeToDisk(key, data) // slow I/O, outside the lock
}
Mantener un lock durante llamadas de red, I/O de disco o un envío a un channel es la causa más común de un programa concurrente lento, y un envío a un channel bajo un lock es una causa habitual de deadlock.
RWMutex para datos de mucha lectura
sync.RWMutex tiene dos modos. RLock/RUnlock toman un lock de lectura compartido que muchas goroutines pueden tener a la vez. Lock/Unlock toman el lock de escritura exclusivo, que espera hasta que salen todos los lectores.
RWMutex compensa cuando dominan las lecturas y cada lectura hace un trabajo real bajo el lock. Para secciones críticas diminutas, como una sola búsqueda en un map, un Mutex normal suele ser igual de rápido, porque el lock de lectura tiene su propia contabilidad. Haz un benchmark antes de elegir.
No puedes convertir un lock de lectura en uno de escritura. Llamar a Lock mientras tienes RLock en la misma goroutine provoca un deadlock. Libera primero el lock de lectura, toma luego el de escritura y vuelve a comprobar la condición, porque otro escritor puede haber cambiado los datos entre medias.
sync/atomic para valores sueltos
Para un contador o un flag, sync/atomic es más sencillo y más barato que un mutex. Los envoltorios tipados (Go 1.19) son los que conviene usar:
Los atómicos protegen un valor cada vez. En cuanto dos valores tienen que cambiar juntos (un saldo y un número de transacciones, un map y su tamaño), usa un mutex. Dos operaciones atómicas separadas pueden intercalarse con otras goroutines entre ellas.
sync.Once
sync.Once ejecuta una función exactamente una vez, por muchas goroutines que la llamen a la vez. Todas las que llaman a Do esperan hasta que ha terminado la primera llamada. Es la forma estándar de inicializar algo de forma perezosa:
Cada línea "runs once" aparece exactamente una vez. Si la función que se pasa a Do provoca un panic, Once la da igualmente por hecha y nunca reintenta. sync.OnceValues hace lo mismo para funciones que devuelven dos valores, normalmente un valor y un error.
Mutex, channel o sync.Map
| Situación | Usa |
|---|---|
| Un struct o un map que varias goroutines actualizan en el sitio | sync.Mutex en el struct |
| Casi todo lecturas, escrituras ocasionales, las lecturas hacen trabajo real | sync.RWMutex |
| Un solo contador o flag | sync/atomic |
| Inicialización única | sync.Once, sync.OnceValue |
| Pasar datos de una goroutine a otra | un channel |
| Una caché cuyas claves se escriben una vez y se leen muchas, o goroutines que tocan claves disjuntas | sync.Map |
sync.Map no es un sustituto general de un map con lock. No tiene parámetros de tipo, así que los valores vuelven como any, y solo es más rápido en los dos casos de la tabla. Empieza con un mutex y un map normal.
Errores comunes
- Copiar un mutex. Pasar por valor un struct que contiene un
sync.Mutex, o usar un receptor por valor, copia el lock.go vetinforma depasses lock by valueocopies lock value. - Bloquear dos veces en la misma goroutine. Los mutex de Go no son reentrantes. Si
Incllama aGety los dos toman el lock,Incse bloquea para siempre. Haz que los métodos públicos tomen el lock y que las funciones auxiliares privadas supongan que ya está tomado. - Olvidar desbloquear en un retorno temprano. Usa
defersalvo que tengas un motivo para no hacerlo. - Bloquear en órdenes distintos. Si una goroutine toma el lock A y luego el B mientras otra toma B y luego A, las dos pueden esperarse mutuamente para siempre. Toma siempre varios locks en el mismo orden.
- Exponer los datos protegidos. Devolver el map interno desde un método permite a quien llama leerlo y escribirlo sin el lock. Devuelve una copia (
maps.Clone, Go 1.21) o un valor suelto. - Proteger solo las escrituras. Las lecturas sin lock concurrentes con escrituras bloqueadas siguen siendo carreras. Ejecuta tus tests con
go test -race.
Preguntas frecuentes
¿Qué es un mutex en Go?
Un sync.Mutex es un lock que deja que solo una goroutine a la vez ejecute el código entre mu.Lock() y mu.Unlock(). Se usa para proteger datos que leen y escriben varias goroutines, como un map o un struct. Su valor cero es un mutex desbloqueado y listo para usar.
¿Cuándo debo usar RWMutex en lugar de Mutex?
Cuando las lecturas superan con mucho a las escrituras y cada lectura mantiene el lock un tiempo apreciable. RLock deja entrar a cualquier número de lectores a la vez, mientras que Lock espera acceso exclusivo. Para secciones críticas cortas, un Mutex normal suele ser igual de rápido o más, así que mide antes de cambiar.
¿Un map de Go es seguro para uso concurrente?
No. Las lecturas concurrentes están bien, pero una escritura concurrente con cualquier otra lectura o escritura es una carrera de datos, y el runtime suele detectarla y caer con fatal error: concurrent map writes (o concurrent map read and map write). Protege el map con un sync.Mutex o un sync.RWMutex, o usa sync.Map para sus casos de uso concretos.
¿sync.Mutex es reentrante en Go?
No. Si una goroutine que tiene el lock vuelve a llamar a Lock, se bloquea para siempre esperándose a sí misma. Organiza el código para que los métodos exportados tomen el lock y llamen a funciones auxiliares no exportadas que suponen que ya está tomado.