ВСЕГДА ли использование `std::sync::Mutex` в точке `await` вызывает взаимоблокировку?

Хотя я прочитал весь ОП, ответ и комментарии Почему у меня возникает взаимоблокировка при использовании 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::join!() — вот что делает его фактически однопоточным (выполняемым в рамках одной и той же задачи tokio). Это называется внутризадачным параллелизмом и представляет собой асинхронную функцию без точного аналога потоковой обработки. Если бы вы обернули f() вызовы в задачи и join! дескрипторы задач, вы получили бы многопоточный параллелизм (в многопоточном исполнителе).
user4815162342 25.04.2024 16:38
Асинхронная передача данных с помощью sendBeacon в JavaScript
Асинхронная передача данных с помощью sendBeacon в JavaScript
В современных веб-приложениях отправка данных из JavaScript на стороне клиента на сервер является распространенной задачей. Одним из популярных...
2
1
101
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

весь код выполняется в однопоточной среде

Действительно.

среда выполнения Tokio по умолчанию является многопоточной

Да, но это касается только задач . Задачи подобны облегченным потокам и могут выполняться параллельно в разных потоках ОС. Но tokio::join!() не создает новых задач. Это асинхронный примитив, который принимает два (или более) фьючерса и объединяет их в одно с помощью конечного автомата для выполнения одной и той же задачи. Преимущество этого метода в том, что он более легкий, но это также означает, что если вы блокируете один из фьючерсов, все остальные также будут заблокированы. Таким образом, это хорошо для кода, действительно привязанного к вводу-выводу. Если код хотя бы немного привязан к ЦП или у вас много фьючерсов, лучше создать задачу.

Также обратите внимание, что задачи tokio могут выполняться в разных потоках, но это не гарантируется. В частности, в Токио есть оптимизация эвристика, которая может привести к использованию только одного потока. В этом случае этот код также заблокируется. Кроме того, даже если он не заблокируется, он все равно блокируется, а блокировку никогда не следует выполнять в асинхронной среде.

Другие вопросы по теме