В однопоточном runtime две задачи используют yield_now().await. Разберите, гарантирует ли этот вызов строгую очерёдность их выполнения.
use tokio::task::yield_now;
#[tokio::main(flavor = "current_thread")]
async fn main() {
tokio::join!(worker("A"), worker("B"));
}
async fn worker(name: &'static str) {
for i in 0..3 {
println!("{name}: {i}");
yield_now().await;
}
}
yield_now().await не гарантирует строгую очерёдность или чередование задач. Он добровольно возвращает управление из текущей async-задачи runtime, обычно позволяя ему продолжить планирование других готовых задач, но конкретный порядок повторного опроса не фиксирован.
Следовательно, программа не обязана печатать строки строго в порядке A0, B0, A1, B1. yield_now улучшает кооперативную справедливость, но не превращает выполнение в параллельное и не задаёт детерминированный планировщик.
Асинхронные runtime обычно используют кооперативное планирование: задача выполняется до тех пор, пока её future не вернёт Poll::Pending или не завершится. Такой подход дешевле вытеснения задачи операционной системой и позволяет обслуживать большое количество ожиданий небольшим числом потоков.
Проблема состоит в том, что runtime не может принудительно прервать произвольный пользовательский код внутри future. Поэтому для длительных вычислительных циклов нужен явный переход управления через .await, включая yield_now().await.
Вызов yield_now полезен, когда задача долго выполняет короткие участки работы и должна дать шанс другим задачам. Однако разработчик может ошибочно принять его за барьер, гарантию переключения на другую задачу или средство синхронизации.
Такое предположение приводит к нестабильному порядку вывода, трудно воспроизводимым тестам и ошибочным протоколам обмена данными. В многопоточном runtime вызов также не гарантирует передачу выполнения конкретному потоку или конкретной задаче.
При достижении yield_now().await future текущей задачи временно возвращает Poll::Pending и планирует её повторную проверку. Runtime получает возможность выбрать другую готовую работу, после чего позднее снова опрашивает эту future.
Это именно подсказка планировщику, а не команда с жёстким контрактом. Runtime может снова выбрать ту же задачу, а содержащие её комбинаторы вроде join! могут иметь собственную логику опроса ветвей до возврата управления наружу. Поэтому порядок сообщений и количество выполненных итераций между переключениями не следует считать гарантированными.
yield_now не создаёт новый поток и не даёт параллельного выполнения. Он также не защищает общие данные, не заменяет канал, mutex или атомарную операцию. Для CPU-bound работы обычно нужен spawn_blocking или отдельный пул вычислительных потоков; для обычного ожидания следует использовать future, которая корректно возвращает Pending до готовности события.
В приведённом коде обе ветви join! находятся внутри одной async-задачи, а не являются независимыми runtime-задачами. Даже если заменить их на отдельные spawn, строгого чередования всё равно не появится: изменится набор планируемых задач, но не появится гарантия порядка.
Сервис обрабатывает очередь запросов в однопоточном runtime. Один обработчик выполняет большой цикл разбора данных без операций ввода-вывода. Пока цикл не завершится, другие запросы не получают процессорное время.
Вариант с добавлением yield_now().await через каждые несколько тысяч элементов прост и сохраняет выполнение в async-контексте, но увеличивает накладные расходы и всё ещё не гарантирует точную справедливость. Вариант с spawn_blocking освобождает async-поток и лучше подходит для действительно CPU-bound работы, но требует учитывать размер пула, передачу владения и стоимость переключения.
Практическое решение: короткие вычислительные фрагменты периодически уступают управление через yield_now, а продолжительные или непредсказуемые вычисления отправляются в spawn_blocking. Синхронизацию состояния при этом обеспечивают отдельными примитивами, а не полагаются на сам факт уступки управления.
1. Обязательно ли после yield_now().await будет опрошена другая задача?
Нет. Вызов создаёт возможность для другого планирования, но не обещает, что runtime выберет конкретную задачу. Даже если другая задача готова, порядок выбора зависит от runtime и текущего механизма планирования.
2. Делает ли yield_now выполнение параллельным на однопоточном runtime?
Нет. На одном потоке задачи лишь поочерёдно получают время процессора. Параллельность требует нескольких потоков или внешнего распределения вычислений; yield_now обеспечивает только кооперативное переключение.
3. Можно ли использовать yield_now для защиты общего счётчика от гонки?
Нет. Уступка управления не является операцией синхронизации и не устанавливает отношения видимости между задачами. Для общего состояния нужны подходящие средства, например передача владения через канал, Mutex, атомарные операции или другой примитив с явно требуемой моделью памяти.