Bloqueando o executor

A maioria dos runtimes async só permite que tarefas de I/O sejam executadas concorrentemente. Isso significa que tarefas que bloqueiam a CPU bloquearão o executor e impedirão que outras tarefas sejam executadas. Uma solução fácil é usar métodos equivalentes async sempre que possível.

use futures::future::join_all;
use std::time::Instant;

async fn sleep_ms(start: &Instant, id: u64, duration_ms: u64) {
    std::thread::sleep(std::time::Duration::from_millis(duration_ms));
    println!(
        "_future_ {id} dormiu por {duration_ms}ms, terminou após {}ms",
        start.elapsed().as_millis()
    );
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let start = Instant::now();
    let sleep_futures = (1..=10).map(|t| sleep_ms(&start, t, t * 10));
    join_all(sleep_futures).await;
}
This slide should take about 10 minutes.
  • Execute o código e veja que os sleeps acontecem consecutivamente em vez de concorrentemente.

  • A variante "current_thread" coloca todas as tarefas em um único thread. Isso torna o efeito mais óbvio, mas o bug ainda está presente na variante multi-threaded.

  • Troque o std::thread::sleep por tokio::time::sleep e aguarde seu resultado.

  • Outra correção seria tokio::task::spawn_blocking que inicia um thread real e transforma seu handle em uma future sem bloquear o executor.

  • Você não deve pensar em tarefas como threads do SO. Elas não mapeiam 1 para 1 e a maioria dos executors permitirá que muitas tarefas sejam executadas em um único thread do SO. Isso é particularmente problemático ao interagir com outras bibliotecas via FFI, onde essa biblioteca pode depender de armazenamento local de thread ou mapear para threads específicos do SO (por exemplo, CUDA). Prefira tokio::task::spawn_blocking em tais situações.

  • Use mutexes sync com cuidado. Manter um mutex sobre um .await pode fazer com que outra tarefa bloqueie, e essa tarefa pode estar sendo executada no mesmo thread.