Concurrencia en Rust con Join y Tokio.
Estoy tratando de ejecutar dos funciones en paralelo con join
.
Mi código es simple:
cargo.toml
tokio = { versión = "1.14.0", características = ["completo"] }
use tokio::join;
use std::thread::sleep;
use std::time::{Duration, Instant};
fn async<em>1() -> i8 {
sleep(Duration::from</em>secs(2));
2
}
fn async<em>2() -> i8 {
sleep(Duration::from</em>secs(2));
1
}
<h1>[tokio::main]</h1>
async fn main() -> () {
let now = Instant::now();
println!("inicio: {:#?}", now.elapsed());
let a = fn<em>1();
let b = fn</em>2();
join!(a, b);
println!("fin: {:#?}", now.elapsed());
}
Pero, sin importar lo que haga, esto tarda 4s —2s + 2s
—, mientras debería tardar 2s si no me equivoco:
inicio: 37ns
fin: 4.01036111s
¿Hay algo que me esté perdiendo?
davy.ai
Sí, estás omitiendo el hecho de que la función
sleep
es una operación bloqueante que pausa el hilo actual, evitando que se ejecuten otras tareas. En este caso, tantofn_1
comofn_2
contienen llamadas bloqueantes desleep
, lo que significa que cada función bloqueará la ejecución de su tarea asociada hasta que haya pasado la duración del sueño.Por lo tanto, cuando llamas a
join!(a, b)
, ambas tareas están bloqueadas esperando a que sus respectivas llamadas desleep
finalicen antes de poder terminar. Como resultado, el tiempo total que tardan ambas tareas en terminar es igual a la suma de sus duraciones de sueño individuales, que son 4 segundos.Para ejecutar dos funciones en paralelo, debes utilizar primitivas de concurrencia asincrónica en lugar de operaciones bloqueantes como
sleep
. Tokio proporciona varias de estas primitivas, incluyendo canales y mutexes, pero quizás la más común y básica sea la funciónspawn
. Aquí tienes un ejemplo de cómo puedes utilizarspawn
para ejecutar dos funciones concurrentemente:En este ejemplo, tanto
fn_1
comofn_2
llaman atask::yield_now()
para permitir que se ejecuten otras tareas mientras esperan. Esto no es estrictamente necesario, pero puede ayudar a mejorar la capacidad de respuesta del sistema en casos en que las tareas de otro modo podrían monopolizar la CPU. La diferencia clave, sin embargo, es que estamos utilizandotask::spawn
para ejecutar cada función como una tarea separada, que se ejecuta de forma independiente y concurrente con otras tareas. Luego utilizamostokio::join!
para esperar a que ambas tareas se completen y recuperar sus resultados. Esto debería resultar en un tiempo total de ejecución de aproximadamente 2 segundos, dado queyield_now
no agrega ninguna demora significativa y el sistema tiene suficientes recursos para ejecutar ambas tareas en paralelo.