Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Асинхронность во встраиваемом Rust

Когда я только начинал эту книгу, я писал большинство примеров, используя только rp-hal. В этой редакции я переписал книгу, сосредоточившись главным образом на асинхронном программировании с Embassy. Официальная книга Embassy уже содержит хорошую документацию, но здесь я хочу дать короткое введение. Давайте кратко посмотрим, что такое асинхронность, и поймём, почему она так ценна во встраиваемых системах.

Представьте, что вы готовите ужин

Если вы знакомы с концепциями параллелизма и асинхронности, эта аналогия вам не нужна: Embassy, по сути, похож на Tokio для встраиваемых систем и предоставляет асинхронную среду выполнения. Если же вы только начинаете знакомиться с асинхронностью, объясню на примере.

Вы готовите ужин и ставите воду кипятиться. Вместо того чтобы стоять и смотреть на кастрюлю, вы режете овощи. Время от времени вы поглядываете на кастрюлю, и, когда появляются пузырьки, вы готовы к следующему шагу. Пока основное блюдо готовится, вы в другой сковороде делаете гарнир. Вы даже проверяете сообщение на телефоне. Вы — один человек, который постоянно переключается между задачами, отслеживает, что требует внимания, делает работу, как только что-то готово, и никогда не стоит без дела, ожидая.

Готовка
Фото: Jason Briscoe на Unsplash

Так и работает асинхронное программирование. Вы — исполнитель (executor), который постоянно решает, что требует внимания. Каждая кулинарная задача — это асинхронная операция. Плита греет без вашего постоянного присмотра. Это и есть неблокирующее ожидание. Вы не застываете на месте, глядя на кипящую воду. Вы занимаетесь другими полезными делами и возвращаетесь, когда всё готово. Ключевая идея — эффективная координация: один человек (исполнитель), несколько ожидающих задач, и вы всегда делаете что-то полезное, переключая внимание на ту задачу, которая готова прямо сейчас. Именно это асинхронное программирование делает для вашего микроконтроллера.

Разные подходы

Во встраиваемых системах микроконтроллер проводит много времени в ожидании. Он ждёт нажатия кнопки, истечения таймера или окончания заданного времени мигания светодиода. Без асинхронности есть два основных подхода.

Блокирующий код

Первый подход — блокирующий код. Программа буквально останавливается и ждёт. Если вы ждёте нажатия кнопки, ваш код крутится в цикле и снова и снова проверяет, изменилось ли состояние кнопки. В это время микроконтроллер не может делать ничего другого. Он не может мигать светодиодом, проверять другие кнопки или реагировать на таймеры. Вся мощность процессора тратится впустую в тесном цикле, который раз за разом спрашивает: «уже готово?»

Прерывания

Второй подход — прямое использование прерываний. Когда происходят аппаратные события, например нажатие кнопки или истечение таймера, запускается обработчик прерывания. Это лучше, потому что основной код может продолжать работать, но код на прерываниях быстро становится сложным и подверженным ошибкам. Нужно тщательно управлять разделяемым состоянием между основным кодом и обработчиками прерываний.

Пока не беспокойтесь о прерываниях. Мы подробно рассмотрим их в следующих главах.

Асинхронность

Асинхронное программирование даёт лучшее из двух миров. Ваш код выглядит чистым и последовательным, как блокирующий код, но на самом деле он не блокируется. Когда вы используете await, код как бы говорит: «мне нужно дождаться этого, но пока можно заняться другой работой». Асинхронная среда выполнения, которую для нас предоставляет Embassy, берёт на себя всю сложность эффективного переключения между задачами.

Как устроена асинхронность в Rust

Когда вы пишете асинхронную функцию на Rust, перед fn ставится ключевое слово async. Внутри этой функции можно использовать ключевое слово await для операций, которые могут занять время. Вот как это выглядит:

#![allow(unused)]
fn main() {
async fn blink_led(mut led: Output<'static>) {
    loop {
        led.set_high();
        Timer::after_millis(500).await;
        led.set_low();
        Timer::after_millis(500).await;
    }
}
}

Важна часть .await. Когда вы пишете Timer::after_millis(500).await, вы говорите среде выполнения: «мне нужно подождать 500 миллисекунд, но в это время процессор мне не нужен». Среда выполнения может тогда запускать другие задачи. Когда 500 миллисекунд пройдут, ваша задача продолжится ровно с того места, где остановилась.

Вспомните нашу аналогию с кухней. Когда вы ставите что-то на плиту и отходите, вы фактически «ожидаете» его готовности. Вы занимаетесь другими делами, а когда всё готово, возвращаетесь к этой задаче. Точно так же, как вы выступаете исполнителем на кухне, отслеживая, что требует внимания и когда, для вашей программы эту роль играет асинхронная среда выполнения.

Embassy

Embassy — одна из популярных асинхронных сред выполнения, которая делает всё это возможным во встраиваемом Rust. Она предоставляет исполнитель, который управляет вашими задачами и обрабатывает аппаратные прерывания.

Исполнитель (Executor)

Когда вы используете #[embassy_executor::main], Embassy автоматически всё настраивает: он запускает ваши задачи, переводит процессор в режим сна, когда все задачи ожидают, и будит его, когда происходят аппаратные события. Исполнитель — координатор, который решает, какую задачу опрашивать и когда. Исполнитель ведёт очередь задач, готовых к выполнению. Когда задача доходит до await и отдаёт управление, исполнитель переходит к следующей готовой задаче. Когда готовых задач нет, исполнитель переводит процессор в режим сна. Прерывания будят исполнитель, который затем опрашивает задачи, ставшие готовыми.

RTIC

RTIC (Real-Time Interrupt-driven Concurrency) — ещё один популярный фреймворк для embedded Rust. В отличие от Embassy, который предоставляет асинхронную среду выполнения вместе с драйверами оборудования, RTIC сосредоточен только на выполнении и планировании. В RTIC вы заранее объявляете задачи с фиксированными приоритетами и разделяемые ресурсы, а фреймворк на этапе компиляции проверяет, что ресурсы разделяются безопасно и без гонок данных. Задачи с более высоким приоритетом могут вытеснять задачи с более низким приоритетом, а планирование выполняется аппаратными прерываниями, что делает тайминги очень предсказуемыми. Поэтому RTIC хорошо подходит для систем жёсткого реального времени, где важны точное управление и детерминированность. Подробнее см. официальную книгу RTIC.

В этой книге мы в основном будем использовать Embassy.