Manejo de Condiciones de Carrera en Go: Implementación de Mutex
Clase 6 de 19 • Curso de Go Avanzado: Concurrencia y Patrones de Diseño
Resumen
¿Cómo crear un sistema de caché concurrente en Go?
La eficiencia en el cálculo de la serie de Fibonacci demuestra los beneficios de un sistema de caché bien diseñado. Sin embargo, cuando introducimos concurrencia, la ejecución puede llevar a condiciones de carrera que afectan la consistencia y fiabilidad del programa. Aquí exploraremos cómo abordar estos desafíos y optimizar un sistema de caché concurrente usando la robusta capacidad de concurrencia de Go.
¿Qué cambios hicimos en nuestra función principal para manejar concurrencia?
Para manipular concurrente las funciones de nuestro sistema de caché, debimos realizar ajustes en la función main
:
-
Creación de WaitGroup: Este grupo nos permite sincronizar las goroutines y asegurarnos de que todas se completen antes de finalizar el programa.
-
Uso de Funciones Anónimas: Introducimos funciones anónimas junto con la palabra clave
go
para ejecutar las operaciones en nuevas goroutines. Este enfoque permite que cada cálculo de Fibonacci se realice de manera concurrente. -
Manejo de Contadores: Se utilizó
wg.Add(1)
para incrementar el contador al iniciar una goroutine ywg.Done()
para decrementar al finalizar, garantizando la correcta espera del programa principal mediantewg.Wait()
.
var wg sync.WaitGroup
for i, n := range numbers {
wg.Add(1)
go func(index, num int) {
defer wg.Done()
calcularFibonacci(num)
}(i, n)
}
wg.Wait()
¿Cómo resolvimos las condiciones de carrera con logs?
Las condiciones de carrera pueden manifestarse cuando varias goroutines acceden a recursos compartidos de forma desincronizada. Para mitigar este problema, implementamos sync.Mutex
en nuestro código:
- Sincronización con Mutex: Antes de acceder a datos compartidos, usamos un lock para asegurar la exclusividad y desbloqueamos justo antes de retornar los resultados. Esto evita que múltiples goroutines modifiquen datos simultáneamente, lo que podría llevar a inconsistencias.
memory := struct{
cache map[int]int
lock sync.Mutex
}{
cache: make(map[int]int),
}
func (m *memory) Get(key int) (int, bool) {
m.lock.Lock()
defer m.lock.Unlock()
val, exists := m.cache[key]
return val, exists
}
- Optimización del uso de locks: Inicialmente, bloqueamos todo el bloque de funcionalidad. Sin embargo, al mejorar, bloqueamos sólo cuando es necesario. Movimos el lock para después de verificar la existencia del valor, reduciendo así el impacto negativo en el rendimiento.
¿Cómo mejoramos el rendimiento del sistema?
Después de establecer los locks, notamos un decremento en el rendimiento debido al bloqueo excesivo:
-
Reanching los locks: Ajustamos los lugares donde se aplicaban los locks, aumentando la eficiencia. Al hacerlo, evitamos bloquear completamente el programa durante operaciones que no lo requieren.
-
Pruebas de carrera: Usamos la herramienta
-race
de Go para verificar que no se presenten nuevas condiciones de carrera, garantizando así la correcta implementación de nuestras mejoras.
go run -race main.go
Este enfoque resultó en un sistema más eficiente que previene el recalculo innecesario de datos y optimiza el uso de memoria.
Con estos cambios, hemos creado un sistema de caché concurrente robusto que utiliza únicamente las librerías estándar de Go. Este sistema, que ahora maneja la concurrencia de manera efectiva, permite almacenar resultados de cálculos previos para evitar cálculos repetitivos y mejorar el rendimiento significativamente. ¡Continúa explorando los vastos horizontes de la programación concurrente en Go!