Хотя я прочитал весь ОП, ответ и комментарии Почему у меня возникает взаимоблокировка при использовании Tokio с std::sync::Mutex?, я пока не понимаю, почему код в ОП блокируется навсегда.
Вот слегка измененная версия исходного кода:
use std::sync::Arc;
use std::sync::Mutex;
// use tokio::sync::Mutex;
use tokio::time::Duration;
async fn f(mtx: Arc<Mutex<i32>>, index: usize) {
println!("{}: trying to lock...", index);
{
let mut v = mtx.lock().unwrap();
// let mut v = mtx.lock().await;
println!("{}: locked", index);
tokio::time::sleep(Duration::from_millis(1)).await;
*v += 1;
}
println!("{}: unlocked", index);
}
#[tokio::main]
async fn main() {
let mtx = Arc::new(Mutex::new(0));
tokio::join!(f(mtx.clone(), 1), f(mtx.clone(), 2));
}
Результат:
1: trying to lock...
1: locked
2: trying to lock...
(and blocks forever...)
Судя по ответу и комментариям (и если я их правильно прочитал), причина в том, что весь код выполняется в однопоточной среде. Если выделенная курсивом часть соответствует действительности, я могу понять поведение блокировки. Однако я не понимаю, действительно ли выделенная курсивом часть соответствует действительности.
Насколько я понимаю,
среда выполнения Tokio по умолчанию является многопоточной, если вы явно не укажете #[tokio::main(flavor = "current_thread")]
(источник)
а выполненные await
задачи можно автоматически переместить в другой рабочий поток (источник ).
Поэтому я думаю, что код НЕ блокируется, если задачи (т. е. f(mtx.clone(), 1).await
, f(mtx.clone(), 2).await
и sleep(...).await
) выбраны (средой выполнения Tokio) для выполнения в разных потоках, но код выглядит блокирующим, поскольку среда выполнения выбирает, все задачи выполняются в та же самая нить.
Верно ли мое понимание?
весь код выполняется в однопоточной среде
Действительно.
среда выполнения Tokio по умолчанию является многопоточной
Да, но это касается только задач . Задачи подобны облегченным потокам и могут выполняться параллельно в разных потоках ОС. Но tokio::join!() не создает новых задач. Это асинхронный примитив, который принимает два (или более) фьючерса и объединяет их в одно с помощью конечного автомата для выполнения одной и той же задачи. Преимущество этого метода в том, что он более легкий, но это также означает, что если вы блокируете один из фьючерсов, все остальные также будут заблокированы. Таким образом, это хорошо для кода, действительно привязанного к вводу-выводу. Если код хотя бы немного привязан к ЦП или у вас много фьючерсов, лучше создать задачу.
Также обратите внимание, что задачи tokio могут выполняться в разных потоках, но это не гарантируется. В частности, в Токио есть оптимизация эвристика, которая может привести к использованию только одного потока. В этом случае этот код также заблокируется. Кроме того, даже если он не заблокируется, он все равно блокируется, а блокировку никогда не следует выполнять в асинхронной среде.
tokio::join!()
— вот что делает его фактически однопоточным (выполняемым в рамках одной и той же задачи tokio). Это называется внутризадачным параллелизмом и представляет собой асинхронную функцию без точного аналога потоковой обработки. Если бы вы обернулиf()
вызовы в задачи иjoin!
дескрипторы задач, вы получили бы многопоточный параллелизм (в многопоточном исполнителе).