Herramientas para manejar Rutinas de Go

rlgino

Gino Luraschi

Posted on May 12, 2023

Herramientas para manejar Rutinas de Go

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
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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 😊 🔧

💖 💪 🙅 🚩
rlgino
Gino Luraschi

Posted on May 12, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related