Программирование RustКонкурентность и asyncRust-разработчик backend-сервисов

В Tokio несколько задач одновременно ждут один async mutex: гарантирован ли порядок FIFO выдачи блокировки?

В Tokio несколько задач одновременно ждут один async mutex: гарантирован ли порядок FIFO выдачи блокировки?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Да. tokio::sync::Mutex гарантирует FIFO-порядок: задачи получают блокировку в том порядке, в котором они вызвали lock. Однако отмена ожидания меняет ситуацию: если задача отменена или её future уничтожена, она теряет место в очереди.

Эта гарантия относится к конкретному mutex Tokio, а не ко всем async mutex в Rust и не к обычному std::sync::Mutex.

Исторический контекст

Обычный mutex блокирует поток операционной системы, пока владелец блокировки не освободит ресурс. Для async runtime это нежелательно: заблокированный рабочий поток не может эффективно выполнять другие async-задачи.

Async mutex был создан для ожидания блокировки без блокирования OS-потока. Tokio дополнительно выбрал предсказуемую FIFO-политику, чтобы снизить риск голодания задач и сделать порядок доступа к ресурсу более стабильным.

Постановка проблемы

Несколько задач могут одновременно обращаться к одному соединению, очереди или другому общему ресурсу. Если порядок выдачи блокировки не определён, отдельная задача потенциально может долго не получать доступ из-за постоянного появления новых конкурентов.

FIFO не устраняет все проблемы. Долгое удержание guard задерживает всех ожидающих, а ожидание блокировки само по себе остаётся асинхронным только до момента получения guard. Кроме того, отменённая задача не продолжает занимать место в очереди.

Подробное решение

При вызове lock Tokio помещает задачу в очередь ожидания, если mutex уже занят. Когда текущий владелец освобождает guard, runtime пробуждает следующего ожидающего согласно порядку постановки в очередь.

Минимальная иллюстрация выглядит так:

use tokio::sync::Mutex; use std::sync::Arc; let value = Arc::new(Mutex::new(0)); let guard = value.lock().await; // Работа с общим состоянием. drop(guard);

Гарантия FIFO не означает, что задача мгновенно начнёт выполняться после пробуждения: её ещё должен запланировать runtime. Также порядок вызова lock определяется фактическим достижением этой операции, а не временем создания задачи.

Если ожидание блокировки отменяется, например через select!, задача удаляется из очереди. При следующей попытке она встанет в очередь заново и может получить другую позицию.

Async mutex стоит выбирать, когда guard действительно должен жить через .await или когда ресурс естественно используется async-задачами. Для короткой синхронной критической секции без .await обычный std::sync::Mutex часто дешевле, поскольку не требует async-очереди и регистрации waker. При этом удерживать любой mutex дольше необходимого не следует.

Ситуация из практики

Несколько async-задач используют общее состояние счётчика. Вариант с std::sync::Mutex может быть эффективен, если блокировка берётся, значение быстро обновляется и guard освобождается до любого .await. Его минус — случайное удержание блокировки во время синхронной операции может заблокировать рабочий поток runtime.

Вариант с tokio::sync::Mutex позволяет ждать блокировку без блокирования OS-потока и даёт FIFO-порядок. Цена — дополнительные накладные расходы и риск надолго задержать очередь, если внутри guard выполняются медленные операции.

Практический выбор — использовать Tokio mutex для действительно асинхронного ресурса, например соединения, которое нужно удерживать через .await, а для коротких операций над небольшим состоянием — обычный mutex и минимальную критическую секцию. Это сохраняет предсказуемость ожидания, не превращая каждую дешёвую операцию в async-синхронизацию.

Что кандидаты часто упускают

1. Сохраняется ли FIFO после отмены ожидания блокировки?

Нет. Отмена удаляет ожидающую задачу из очереди. Если затем она снова вызывает lock, это уже новая попытка с новой позицией. Поэтому нельзя строить протокол, предполагающий, что отменённая задача сохранит своё место.

2. Означает ли FIFO, что задачи завершат критические секции в том же порядке?

Нет. FIFO определяет порядок получения mutex, но не порядок завершения всей работы задачи. После освобождения guard задача может быть вытеснена, ждать другой ресурс или выполнять дальнейшие операции, поэтому наблюдаемый порядок внешних событий может отличаться.

3. Гарантирует ли FIFO отсутствие задержек и голодания?

FIFO уменьшает риск голодания именно в очереди данного mutex, но не гарантирует малое время ожидания. Долгая критическая секция, приостановка runtime или медленная операция внутри guard задержат всех следующих участников. Поэтому FIFO — гарантия порядка, а не гарантия производительности или ограниченной задержки.