Неожиданное поведение tokio::задача::spawn_blocking

#rust #rust-tokio

Вопрос:

Я экспериментирую с tokio tokio::spawn и tokio::task::spawn , оказывается, не понимаю, как ведет себя последний.

Когда я запускаю следующий код:

 #[tokio::main]
pub async fn main() {
    // I'm spawning one block of functions
    let h = tokio::task::spawn_blocking(move || {
        block_one();
    });

    // and another block of functions
    let h2 = tokio::spawn(async move {
        block_two().await;
    });

    // then I collect the handles
    h.await.unwrap();
    h2.await.unwrap();
}

#[tokio::main] //needed as this block is not treated as syncronous by main
pub async fn block_one() {
    let mut handles = vec![];

    for i in 1..10 {
        let h = tokio::spawn(async move {
            println!("Starting func #{}", i);
            i_take_random_time().await;
            println!("Ending func #{}", i);
        });
        handles.push(h);
    }

    for h in handles {
        h.await.unwrap();
    }
}

pub async fn block_two() {
    let mut handles = vec![];

    for i in 10001..10010 {
        let h = tokio::spawn(async move {
            println!("Starting func #{}", i);
            i_take_random_time().await;
            println!("Ending func #{}", i);
        });
        handles.push(h);
    }

    for h in handles {
        h.await.unwrap();
    }
}
 

Я ожидаю, что первый блок функций будет запущен в полном объеме — только тогда будет запущен второй блок. Вот как я понимаю «spawn_blocking» — он блокирует дальнейшее выполнение до тех пор, пока все, что находится внутри, не будет выполнено.

Что я на самом деле получаю, так это то, что сначала запускается второй блок функций (полностью, все 10 из них) — только затем запускается первый блок. Так что все в точности наоборот по сравнению с тем, что я ожидал.

Чтобы еще больше запутать ситуацию, когда я изменяю приведенный выше код spawn_blocking для обоих блоков — все 20 функций запускаются вместе, как будто оба блока являются частью одного большого асинхронного цикла. Опять не то, что я ожидал — я бы подумал, что первый блок запустится, блокируется до того, как это будет сделано, а затем запустится второй.

Может кто-нибудь помочь мне расшифровать, что происходит?

Полный код для воспроизведения 2 описанных выше сценариев доступен в этом репозитории.

  • сценарий 5 = первый случай, который я описал
  • сценарий 6 = второй случай, который я описал

Примечание: здесь существует два уровня асинхронности: МЕЖДУ блоками и ВНУТРИ блоков. Надежда помогает избежать любой путаницы.

Комментарии:

1. Я еще не прочитал весь ваш вопрос целиком, но вы почти наверняка не захотите комментировать block_one #[tokio::main] его . Обычно это делается только для реальной main функции вашего исполняемого файла, поскольку она создает для вас среду выполнения. Вполне вероятно, что у вас есть (по крайней мере) два времени выполнения здесь, что может объяснить некоторое неопределенное поведение.

2. Использование #[tokio::main] действительно не то, что вам нужно, так как оно порождает новую среду выполнения и целую кучу потоков, но это не причина путаницы.

Ответ №1:

Это звучит так, как будто вы ожидаете spawn_blocking заблокировать запуск других вещей, но его цель прямо противоположна. Цель spawn_blocking состоит в том, чтобы не блокировать запуск других вещей.

Основное место, где spawn_blocking используется, предназначено для операций, которые в противном случае заблокировали бы поток из-за использования несинхронных операций, таких как std::net . Он делает это, выгружая их в отдельный пул потоков. Название происходит от того факта, что вы создаете операцию блокировки, чтобы она могла выполняться в другом месте.

Чтобы дождаться завершения первого блока, вы должны сделать это:

 #[tokio::main]
pub async fn main() {
    // I'm spawning one block of functions
    let h = tokio::task::spawn_blocking(move || {
        block_one();
    });

    // wait for the first block
    h.await.unwrap();

    // then spawn another block of functions
    let h2 = tokio::spawn(async move {
        block_two().await;
    });

    h2.await.unwrap();
}
 

Обратите внимание, что использование #[tokio::main] (или block_on ) непосредственно внутри spawn_blocking очень редко то, что вы хотите. Просто порождайте обычную задачу tokio::spawn .

Комментарии:

1. Теперь все сходится. Спасибо, Элис.