Go
Tutorial Go: Selectores y Mutexes Concurrencia y Exclusión Mutua
Aprende a usar selectores y mutexes en Go para manejar concurrencia y exclusión mutua, mejorando el control sobre canales y variables compartidas en GoLang.
Aprende Go GRATIS y certifícateUso de select
En Go, el uso del select
permite gestionar múltiples operaciones de canal de forma concurrente, lo que es esencial para evitar bloqueos. El select
actúa de manera similar a una instrucción switch
, pero está diseñado específicamente para trabajar con canales. Esto significa que puedes esperar en múltiples operaciones de envío o recepción al mismo tiempo, y el select
elegirá una de ellas que esté lista para proceder.
El funcionamiento básico del select
es evaluar múltiples casos de canal. Cada caso corresponde a una operación de envío o recepción. Si más de un caso está listo, el select
elige uno al azar, lo que ayuda a distribuir la carga de trabajo de manera uniforme. Si ningún caso está listo, el select
se bloqueará hasta que al menos uno lo esté, a menos que exista un caso default, que se ejecutará inmediatamente si está presente.
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "mensaje de ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "mensaje de ch2"
}()
select {
case msg1 := <-ch1:
fmt.Println("Recibido:", msg1)
case msg2 := <-ch2:
fmt.Println("Recibido:", msg2)
}
}
Salida esperada:
Recibido: mensaje de ch1
Recibido: Mensaje único
En este ejemplo, dos goroutines envían mensajes a través de dos canales diferentes, ch1
y ch2
, después de un tiempo específico. El select
espera a que uno de los dos canales reciba un mensaje. Importante: el uso del select
asegura que el programa no se bloqueará esperando en un solo canal, sino que responderá al primero que esté listo.
Además, select
permite implementar timeouts para operaciones que podrían tardar demasiado. Esto se logra utilizando el paquete time
y creando un caso adicional en el select
:
select {
case msg1 := <-ch1:
fmt.Println("Mensaje recibido:", msg1)
case <-time.After(5 * time.Second):
fmt.Println("Timeout: no se recibió ningún mensaje en 5 segundos")
}
Salida esperada:
Recibido: mensaje de ch1
En este código, si no se recibe ningún mensaje en el canal dentro de los 5 segundos, el caso del timeout se ejecuta. Esto es útil para garantizar que el programa no se bloquee indefinidamente esperando una operación que podría no completarse.
El caso default en un select
se ejecuta inmediatamente si ninguno de los demás casos está listo. Esto es útil para evitar bloqueos cuando se desea realizar otras tareas si no hay canales listos:
select {
case msg1 := <-ch1:
fmt.Println("Mensaje recibido:", msg1)
default:
fmt.Println("No hay mensajes disponibles; realizando otras tareas")
}
Salida esperada:
Recibido: mensaje de ch1
Este ejemplo demuestra cómo el caso default permite que el flujo de ejecución continúe sin esperar indefinidamente a que el canal ch1
reciba un mensaje.
El select
también es fundamental para implementar patrones avanzados de concurrencia, como multiplexación de canales y gestión de cancelaciones. Al permitir que una goroutine escuche múltiples canales, se puede diseñar un sistema que responda de manera dinámica a diferentes eventos y condiciones.
Uso de mutexes
En Go, la sincronización explícita es crucial cuando múltiples goroutines acceden a variables compartidas.
Las condiciones de carrera ocurren cuando dos o más goroutines intentan leer y escribir en una variable al mismo tiempo, lo que puede llevar a comportamientos indeterminados y errores difíciles de rastrear. Para prevenir estas condiciones, Go proporciona la estructura sync.Mutex
, que permite la exclusión mutua en el acceso a recursos compartidos.
Un Mutex
es un mecanismo de bloqueo que asegura que solo una goroutine pueda acceder a una sección crítica de código a la vez. Al usar un Mutex
, se deben seguir dos operaciones fundamentales: Lock
y Unlock
. La operación Lock
bloquea el acceso a la sección crítica, y Unlock
libera este bloqueo, permitiendo que otras goroutines accedan al recurso.
package main
import (
"fmt"
"sync"
)
var (
contador int
mutex sync.Mutex
)
func incrementar(wg *sync.WaitGroup) {
defer wg.Done()
mutex.Lock()
contador++
mutex.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go incrementar(&wg)
}
wg.Wait()
fmt.Println("Valor final del contador:", contador)
}
En este ejemplo, se define una variable contador
que es incrementada por múltiples goroutines. El uso del Mutex
garantiza que solo una goroutine pueda modificar el valor de contador
a la vez, evitando condiciones de carrera.
Importante: siempre que se use un mutex, es esencial asegurarse de que cada
Lock()
tenga un correspondienteUnlock()
para evitar bloqueos permanentes. Una práctica común es usardefer
inmediatamente después deLock()
para garantizar queUnlock()
se llame cuando la función regrese, incluso si ocurre un error.
Además, Go ofrece el tipo sync.RWMutex
, que permite un mayor control sobre el acceso concurrente a los recursos. Un RWMutex
proporciona dos tipos de bloqueos: RLock
para operaciones de solo lectura y Lock
para operaciones de escritura. Esto permite que múltiples goroutines puedan leer simultáneamente de un recurso, pero asegura que solo una goroutine pueda escribir en él.
package main
import (
"fmt"
"sync"
)
var (
contador int
rwMutex sync.RWMutex
)
func leerContador() int {
rwMutex.RLock()
defer rwMutex.RUnlock()
return contador
}
func incrementarContador(wg *sync.WaitGroup) {
defer wg.Done()
rwMutex.Lock()
contador++
rwMutex.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go incrementarContador(&wg)
}
wg.Wait()
fmt.Println("Valor final del contador:", leerContador())
}
En este segundo ejemplo, se utiliza sync.RWMutex
para permitir lecturas concurrentes mientras se asegura que las escrituras sean exclusivas. Esto es útil en escenarios donde las operaciones de lectura son más frecuentes que las de escritura, mejorando así el rendimiento sin comprometer la seguridad de los datos.
El uso de mutexes es fundamental para evitar las condiciones de carrera, que pueden dar lugar a resultados inesperados o erróneos en programas concurrentes. Garantizan que las operaciones sobre variables compartidas sean atómicas y seguras, lo que es esencial para mantener la consistencia de los datos en un entorno concurrente.
Ejemplos de patrones concurrentes
En aplicaciones concurrentes desarrolladas en Go, es común combinar el uso de selectores y mutexes para manejar operaciones en canales y proteger datos compartidos, respectivamente.
Estos dos mecanismos se utilizan para abordar diferentes aspectos de la concurrencia:
- el select para elegir entre múltiples operaciones de canal
- los mutexes para garantizar la exclusión mutua en el acceso a recursos compartidos.
Patrón de multiplexación de canales con protección de datos compartidos
Supongamos que tenemos múltiples fuentes de datos que alimentan a una variable compartida, y necesitamos actualizar esta variable sin generar condiciones de carrera. Podemos utilizar un select para escuchar múltiples canales y un mutex para sincronizar el acceso a la variable compartida.
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var (
valorCompartido int
mutex sync.Mutex
)
func generarDatos(canal chan int, id int) {
for {
tiempoEspera := time.Duration(rand.Intn(1000)) * time.Millisecond
time.Sleep(tiempoEspera)
dato := rand.Intn(100)
fmt.Printf("Goroutine %d generó dato %d\n", id, dato)
canal <- dato
}
}
func main() {
canal1 := make(chan int)
canal2 := make(chan int)
go generarDatos(canal1, 1)
go generarDatos(canal2, 2)
for {
select {
case dato := <-canal1:
mutex.Lock()
valorCompartido += dato
fmt.Printf("Valor actualizado desde canal1: %d\n", valorCompartido)
mutex.Unlock()
case dato := <-canal2:
mutex.Lock()
valorCompartido += dato
fmt.Printf("Valor actualizado desde canal2: %d\n", valorCompartido)
mutex.Unlock()
}
}
}
En este ejemplo, dos goroutines generan datos aleatorios y los envían a través de canales independientes. El select en el main
escucha ambos canales y, cuando recibe datos, bloquea el acceso a valorCompartido
utilizando el mutex, actualiza su valor y luego libera el mutex. De esta manera, se garantiza la consistencia del dato compartido pese al acceso concurrente.
Patrón de trabajador concurrente con canal de tareas y mutex de estado
Otra situación común es la necesidad de distribuir tareas entre varios trabajadores concurrentes y administrar el estado compartido. Aquí se muestra cómo implementar un pool de trabajadores que procesan tareas de un canal y actualizan un contador compartido protegido por un mutex.
package main
import (
"fmt"
"sync"
)
var (
contadorExitos int
mutex sync.Mutex
wg sync.WaitGroup
)
func trabajador(id int, tareas <-chan int) {
defer wg.Done()
for tarea := range tareas {
// Procesamiento de la tarea
fmt.Printf("Trabajador %d procesando tarea %d\n", id, tarea)
// Simular éxito en el procesamiento
mutex.Lock()
contadorExitos++
mutex.Unlock()
}
}
func main() {
numTrabajadores := 3
tareas := make(chan int, 10)
for i := 1; i <= numTrabajadores; i++ {
wg.Add(1)
go trabajador(i, tareas)
}
for i := 1; i <= 20; i++ {
tareas <- i
}
close(tareas)
wg.Wait()
fmt.Printf("Número total de tareas exitosas: %d\n", contadorExitos)
}
En este código, se crean varias goroutines como trabajadores que reciben tareas de un canal. Cada trabajador procesa una tarea y actualiza un contador compartido. El acceso al contador compartido está protegido por un mutex para evitar condiciones de carrera. Este patrón es útil para aprovechar múltiples núcleos y mejorar el desempeño en operaciones intensivas.
Patrón de cancelación controlada con select y mutexes
En ocasiones, es necesario poder cancelar operaciones concurrentes de manera segura. Utilizando un canal de señalización y un select, podemos coordinar la terminación de goroutines y asegurar que los recursos compartidos se gestionen adecuadamente con mutexes.
package main
import (
"fmt"
"sync"
"time"
)
var (
contador int
mutex sync.Mutex
)
func proceso(id int, cancel <-chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-cancel:
fmt.Printf("Proceso %d cancelado\n", id)
return
default:
// Trabajo simulado
time.Sleep(500 * time.Millisecond)
mutex.Lock()
contador++
fmt.Printf("Proceso %d incrementó contador a %d\n", id, contador)
mutex.Unlock()
}
}
}
func main() {
cancel := make(chan struct{})
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go proceso(i, cancel, &wg)
}
time.Sleep(2 * time.Second)
close(cancel) // Señal de cancelación
wg.Wait()
fmt.Println("Todos los procesos han terminado")
}
En este ejemplo, múltiples goroutines ejecutan un proceso que incrementa un contador compartido. Utilizando un canal de cancelación, podemos indicar a las goroutines que deben terminar su ejecución. El select dentro de cada goroutine verifica si se ha recibido la señal de cancelación y, en tal caso, finaliza de manera controlada. El acceso al contador está protegido por un mutex, asegurando la integridad de los datos hasta el final.
Patrón de productor-consumidor con buffers y sincronización
Una variación común es implementar el patrón productor-consumidor, donde uno o más productores generan datos que son consumidos por uno o más consumidores. El uso de buffers y la sincronización adecuada es crucial para evitar bloqueos y condiciones de carrera.
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func main() {
buffer := make(chan int, 10)
var wgProductores, wgConsumidores sync.WaitGroup
// Iniciar productores
for i := 1; i <= 2; i++ {
wgProductores.Add(1)
go productor(i, buffer, &wgProductores)
}
// Iniciar consumidores
for i := 1; i <= 3; i++ {
wgConsumidores.Add(1)
go consumidor(i, buffer, &wgConsumidores)
}
wgProductores.Wait()
close(buffer)
wgConsumidores.Wait()
fmt.Println("Todos los productores y consumidores han terminado")
}
func productor(id int, buffer chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
tiempoEspera := time.Duration(rand.Intn(500)) * time.Millisecond
time.Sleep(tiempoEspera)
dato := rand.Intn(100)
buffer <- dato
fmt.Printf("Productor %d produjo dato %d\n", id, dato)
}
}
func consumidor(id int, buffer <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for dato := range buffer {
fmt.Printf("Consumidor %d recibió dato %d\n", id, dato)
// Procesar dato...
}
}
En este código, los productores generan datos y los envían a través de un canal con buffer. Los consumidores reciben los datos del canal y los procesan. El canal con buffer actúa como un almacén que regula el flujo entre productores y consumidores, evitando bloqueos cuando los consumidores no pueden procesar datos inmediatamente. Aunque en este ejemplo no se hace uso de mutexes, si hubiera una variable compartida entre productores y consumidores, se debería proteger con un mutex.
Patrón de uso de sync.Cond para coordinación avanzada
Para casos donde se requiere una coordinación más sofisticada entre goroutines, Go proporciona sync.Cond
, que permite que una goroutine espere hasta que se cumpla una condición determinada, mientras otra goroutine la notifica cuando esa condición cambia. Esto es útil para implementar patrones de espera y señalización.
package main
import (
"fmt"
"sync"
)
type almacen struct {
datos []int
mutex sync.Mutex
cond *sync.Cond
}
func nuevoAlmacen() *almacen {
a := &almacen{}
a.cond = sync.NewCond(&a.mutex)
return a
}
func (a *almacen) producir(dato int) {
a.mutex.Lock()
a.datos = append(a.datos, dato)
fmt.Printf("Producido: %d\n", dato)
a.mutex.Unlock()
a.cond.Signal()
}
func (a *almacen) consumir() {
a.mutex.Lock()
for len(a.datos) == 0 {
a.cond.Wait()
}
dato := a.datos[0]
a.datos = a.datos[1:]
fmt.Printf("Consumido: %d\n", dato)
a.mutex.Unlock()
}
func main() {
a := nuevoAlmacen()
var wg sync.WaitGroup
// Consumidor
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 5; i++ {
a.consumir()
}
}()
// Productor
for i := 1; i <= 5; i++ {
a.producir(i)
}
wg.Wait()
fmt.Println("Proceso completado")
}
En este ejemplo, implementamos un almacén donde un consumidor espera hasta que haya datos disponibles. Utilizando sync.Cond
, el consumidor puede esperar de manera eficiente sin consumir recursos de CPU, y el productor puede notificar cuando hay nuevos datos. El uso combinado de mutex y condicionales proporciona una manera robusta de coordinar acciones entre múltiples goroutines.
El uso combinado de selectores y mutexes aseguran que las goroutines respondan a eventos externos de manera concurrente mientras protegen el acceso a los datos compartidos, evitando condiciones de carrera. Este enfoque es particularmente útil en sistemas donde el flujo de datos y las señales de control deben gestionarse de manera coordinada.
Ejercicios de esta lección Selectores y Mutexes Concurrencia y Exclusión Mutua
Evalúa tus conocimientos de esta lección Selectores y Mutexes Concurrencia y Exclusión Mutua con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Cadenas de texto y manipulación
Selectores y mutexes: concurrencia y exclusión
Agenda de contactos por consola
Composición de structs en lugar de herencia
Estructuras de control
Arrays y slices
Control de flujo y estructuras de bucle
Sistema API REST gestión de libros
Métodos con receptores por valor y por puntero
API REST con net/http
Generics
Evaluación Go
Métodos HTTP con net/http
Crear e invocar funciones
Operadores y expresiones
Polimorfismo a través de Interfaces
Manejo explícito de errores
Estructuras structs
Tipos de datos, variables y constantes
Introducción a Go
Canales y comunicación entre Goroutines
Condiciones de carrera
Punteros y referencias
Goroutines y concurrencia básica
Instalación Go primer programa
Errores personalizados y trazabilidad
Estructuras de datos Mapas
Cliente de API OpenWeatherMap clima
Todas las lecciones de Go
Accede a todas las lecciones de Go y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Go
Introducción Y Entorno
Instalación Y Primer Programa De Go
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Cadenas De Texto Y Manipulación
Sintaxis
Estructuras De Control
Sintaxis
Control De Flujo Y Estructuras De Bucle
Sintaxis
Funciones
Sintaxis
Arrays Y Slices
Estructuras De Datos
Mapas
Estructuras De Datos
Punteros Y Referencias
Estructuras De Datos
Estructuras Structs
Programación Orientada A Objetos
Métodos Con Receptores Por Valor Y Por Puntero
Programación Orientada A Objetos
Polimorfismo A Través De Interfaces
Programación Orientada A Objetos
Composición De Structs En Lugar De Herencia
Programación Orientada A Objetos
Generics
Programación Orientada A Objetos
Manejo Explícito De Errores
Manejo De Errores Y Excepciones
Errores Personalizados Y Trazabilidad
Manejo De Errores Y Excepciones
Métodos Http Con Net/http
Comunicación Por Http
Api Rest Con Net/http
Comunicación Por Http
Goroutines Y Concurrencia Básica
Concurrencia Y Paralelismo
Canales Y Comunicación Entre Goroutines
Concurrencia Y Paralelismo
Condiciones De Carrera
Concurrencia Y Paralelismo
Selectores Y Mutexes Concurrencia Y Exclusión Mutua
Concurrencia Y Paralelismo
Evaluación Conocimientos Go
Evaluación
Certificados de superación de Go
Supera todos los ejercicios de programación del curso de Go y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender cómo utilizar
select
para gestionar múltiples canales y evitar bloqueos en Go. - Implementar mecanismos de timeout y casos por defecto en
select
. - Utilizar
sync.Mutex
para garantizar exclusión mutua en el acceso a variables compartidas. - Aplicar
sync.RWMutex
para operaciones de lectura concurrentes. - Desarrollar patrones concurrentes combinando selectores y mutexes.
- Implementar patrones avanzados de concurrencia como productor-consumidor y cancelación controlada.