#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. Теперь все сходится. Спасибо, Элис.