Introducción
Como expliqué en algún post anterior, Golang ofrece herramientas muy interesantes y útiles, que nos ayudan a desarrollar nuestras aplicaciones. Por otro lado, también mencioné que Golang ofrece muchas facilidades para manejo de concurrencia, acá veremos algunas de esas herramientas que harán de nuestro trabajo concurrente más robusto.
Herramientas
En este post vamos a mencionar solo 3 de las muchas herramientas que ofrece Go para el manejo de concurrencia, sin entrar demasiado en detalle, próximamente planeo adentrarme un poco más en estos.
Rutinas de Go
Primero para entender cómo son y cómo funcionan las rutinas en golang, escribí sobre esto en un post anterior.
Canales
Partiendo de lo que son rutinas (threads virtuales livianos) los canales son, como su nombre lo índica, canales de comunicación entre estas rutinas, es una herramienta poderosa, porque cuando empezamos a ejecutar nuestras rutinas en paralelo, a veces nos encontraremos con la necesidad de compartir recursos entre estas rutinas.
Ante todo el slogan de la concurrencia en Golang es "No comunicarse compartiendo la memoria, compartir la memoria al comunicarse."
Habiendo dicho esto, podemos partir del siguiente ejemplo que ilustra el funcionamiento de un canal en Go:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
messages := make(chan string)
// Acá se ejecuta la rutina y se pasa el canal
go sendString(messages)
// Acá se recibe el valor que se envío por el canal
// Y se guarda en la variable msg
msg := <-messages
// Mientras esperamos recibir algo en el canal `messages` el thread queda bloqueado
fmt.Printf("Message received %s", msg)
fmt.Println("Another actions...")
}
func sendString(msg chan string) {
text := readFromConsole()
// Dentro del canal, enviamos el valor por el canal
// Y seguimos el hilo de nuestra rutina
msg <- text
}
func readFromConsole() string {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter text: ")
text, _ := reader.ReadString('\n')
return text
}
En este ejemplo, vemos cuando nuestro método sendString
pasa al canal msg
el valor del texto que leímos por consola. Luego, en el thread principal de la función main
al recibir el valor, desbloqueamos el thread principal que queda bloqueado.
Wait Groups
En cuanto a los wait groups, podemos definir sincronía entre los canales de manera que podamos ejecutarlos y esperar en caso de haber dependencia entre ellos. El funcionamiento es tan simple como declarar nuestro WaitGroup
y después agregar rutinas a nuestro WaitGroup
, posteriormente tenemos que avisar a nuestro grupo que cada rutina terminó.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var resp1 string
var resp2 string
// Declaramos nuestro WaitGroup
wg := sync.WaitGroup{}
// Avisamos que el límite será 2 rutinas
wg.Add(2)
go func() {
resp1 = askToService1()
// Avisamos que esta rutina terminó
wg.Done()
}()
go func() {
resp2 = askToService2()
// Avisamos que esta rutina terminó
wg.Done()
}()
// Esperamos a que las ejecuciones terminen
wg.Wait()
// Seguimos la ejecución
fmt.Printf("Response 1: %s\n", resp1)
fmt.Printf("Response 2: %s\n", resp2)
fmt.Println("Another actions...")
}
func askToService1() string {
time.Sleep(3 * time.Second)
return "Hello"
}
func askToService2() string {
time.Sleep(4 * time.Second)
return "Bye bye"
}
Este es un caso práctico que hemos desarrollado en una de mis experiencias laborales, por lo que es un caso práctico y concreto sobre el uso de WaitGroup
.
Mutex
Mutex es un término propio de ejecuciones del sistema operativo que se empeña para evitar las condiciones de carrera, lo que haremos es crear mutex para saber cuando un recurso está siendo accedido por otro hilo de ejecución. De esta manera, bloqueamos un recurso, y en caso de que ese recurso ya esté bloqueado vamos a esperar a que esté disponible.
package main
import (
"fmt"
"sync"
)
type Service struct {
// Este mutex será el que bloquee el recurso `counters`
mu sync.Mutex
counters map[string]int
}
func (svc *Service) increase(counterName string) {
// Acá es donde bloqueamos el recurso
// En caso de estar bloqueado por otra rutina, esperamos hasta desbloquearlo
svc.mu.Lock()
// El defer nos permite ejecutar el comando al terminar la función
// Y con el mutex.Unlock() desbloqueamos el recurso
defer svc.mu.Unlock()
svc.counters[counterName]++
}
func (svc *Service) doIncrement(name string, limit int) {
for i := 0; i < limit; i++ {
svc.increase(name)
}
}
func main() {
c := Service{
counters: map[string]int{"counterA": 0, "counterB": 0},
}
var wg sync.WaitGroup
wg.Add(3)
go func() {
c.doIncrement("counterA", 1000)
wg.Done()
}()
go func() {
c.doIncrement("counterA", 1000)
wg.Done()
}()
go func() {
c.doIncrement("counterB", 1000)
wg.Done()
}()
wg.Wait()
fmt.Println(c.counters)
}
En el ejemplo, se nota como c.doIncrement("counterA", 1000)
es prácticamente accedido al mismo tiempo 2 veces, para no pisar su valor, el mu.Lock()
va a esperar hasta que se desbloquee el recurso.
Conclusión
En este post repasamos algunas de las herramientas de Go para manejo de concurrencia, pero también existen otras herramientas que posiblemente veremos más adelante. Así mismo, planeo adentrarme en más detalle acerca de estas herramientas, por lo que te recomiendo suscribirte y estar atento a los siguientes posts 😊 🔧
Top comments (0)