es.davy.ai

Preguntas y respuestas de programación confiables

¿Tienes una pregunta?

Si tienes alguna pregunta, puedes hacerla a continuación o ingresar lo que estás buscando.

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?

Tags:  , , ,

Answer

  1. Avatar for 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, tanto fn_1 como fn_2 contienen llamadas bloqueantes de sleep, 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 de sleep 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ón spawn. Aquí tienes un ejemplo de cómo puedes utilizar spawn para ejecutar dos funciones concurrentemente:

    use tokio::{task, time::Instant};
    
    async fn fn_1() -> i8 {
        task::yield_now().await; // permitir que se ejecuten otras tareas
        2
    }
    
    async fn fn_2() -> i8 {
        task::yield_now().await; // permitir que se ejecuten otras tareas
        1
    }
    
    #[tokio::main]
    async fn main() {
        let now = Instant::now();
        println!("inicio: {:#?}", now.elapsed());
    
        let a = task::spawn(async { fn_1().await });
        let b = task::spawn(async { fn_2().await });
    
        let (resultado_a, resultado_b) = tokio::join!(a, b);
    
        println!("fin: {:#?}", now.elapsed());
        println!("resultados: {}, {}", resultado_a.unwrap(), resultado_b.unwrap());
    }
    

    En este ejemplo, tanto fn_1 como fn_2 llaman a task::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 utilizando task::spawn para ejecutar cada función como una tarea separada, que se ejecuta de forma independiente y concurrente con otras tareas. Luego utilizamos tokio::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 que yield_now no agrega ninguna demora significativa y el sistema tiene suficientes recursos para ejecutar ambas tareas en paralelo.

Comments are closed.