Concurrencia en V: spawn, canales, select y shared con lock

V ofrece dos formas de ejecutar código de forma concurrente, con matices que vale la pena diferenciar antes de escribir nada: go, para hilos ligeros gestionados por el runtime de V, y spawn, para hilos de sistema operativo reales. En la práctica, el código de ejemplo y las librerías de V usan casi siempre spawn, así que es donde nos vamos a centrar. Si vienes de Go, vas a reconocer buena parte del vocabulario (canales, select), aunque la forma de esperar resultados es distinta.

Lanzar hilos con spawn

fn get_hypot(x f64, y f64) f64 {
    return math.sqrt(x * x + y * y)
}

fn main() {
    h := spawn get_hypot(54.06, 2.08)
    result := h.wait() // bloquea hasta que el hilo termina
    println(result)
}

La diferencia con la goroutine de Go es el manejo del resultado: en Go necesitas un canal para recuperar el valor de retorno de una goroutine, en V el propio handle que devuelve spawn tiene un método .wait() que te da el resultado directamente.

Múltiples hilos a la vez

mut threads := []thread{}
threads << spawn task(1, 500)
threads << spawn task(2, 300)
threads.wait() // espera a que todos terminen

// Con tipo de retorno, threads.wait() devuelve un array de resultados
mut typed_threads := []thread int{}
for i in 1 .. 10 {
    typed_threads << spawn expensive_computing(i)
}
results := typed_threads.wait() // []int

Este patrón de array tipado de threads es más directo que el equivalente en Go con sync.WaitGroup más un slice de resultados protegido por mutex: V te da los resultados ya ensamblados en el orden en que lanzaste los hilos.

Canales: la misma idea que en Go

ch := chan int{}              // sin buffer
ch2 := chan f64{cap: 100}     // buffer de 100

ch <- 42                       // enviar (bloquea si no hay buffer libre)
value := <-ch or { -1 }        // recibir, con manejo de error si el canal está cerrado
ch.close()

La sintaxis chan Type{} y las flechas <- son prácticamente un calco de Go. La diferencia está en la recepción: V trata leer de un canal cerrado como un caso que pasa por el bloque or, coherente con el sistema de manejo de errores que vimos en el artículo anterior de la serie.

select: escuchar varios canales a la vez

select {
    a := <-ch {
        println('recibido de ch: ${a}')
    }
    b = <-ch2 {
        println('recibido de ch2: ${b}')
    }
    ch3 <- c {
        println('enviado a ch3')
    }
    500 * time.millisecond {
        println('timeout')
    }
}

Funciona igual que el select de Go: elige la primera rama que esté lista, y la rama de timeout evita bloqueos indefinidos si ningún canal responde a tiempo.

Estado compartido sin carreras: shared y lock

Para compartir una variable entre hilos sin pasar por canales, V tiene la palabra clave shared, con bloques lock y rlock que el compilador obliga a usar antes de tocar la variable:

struct Counter {
mut:
    value int
}

shared counter := Counter{}

fn increment(shared c Counter) {
    lock c {
        c.value += 1
    }
}

fn read_only(shared c Counter) int {
    rlock c {
        return c.value
    }
}

Es una diferencia de diseño notable frente a Go: en Go, un sync.Mutex es una convención que puedes olvidar proteger; en V, acceder a una variable shared fuera de un bloque lock o rlock es un error de compilación. Es la misma filosofía que Mutex<T> en Rust, donde el dato vive envuelto por el propio mutex y el compilador no te deja tocarlo sin pedir el lock antes.

Si quieres comparar con más detalle, tenemos este artículo sobre patrones de concurrencia en Go (worker pools, fan-out/fan-in) y este otro sobre Mutex, RwLock y canales en Rust. En el próximo artículo de la serie tocamos veb, el framework web de V que sustituyó recientemente a vweb.

Imagen: Pexels / Tima Miroshnichenko

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP