From e73e490ff7640a3f3bfe2246fee7350952bfde47 Mon Sep 17 00:00:00 2001 From: TakiMoysha <36836047+TakiMoysha@users.noreply.github.com> Date: Sat, 16 Aug 2025 03:17:05 +0200 Subject: [PATCH 1/9] translated part post-12 --- .../posts/12-async-await/index.ru.md | 1825 +++++++++++++++++ 1 file changed, 1825 insertions(+) create mode 100644 blog/content/edition-2/posts/12-async-await/index.ru.md diff --git a/blog/content/edition-2/posts/12-async-await/index.ru.md b/blog/content/edition-2/posts/12-async-await/index.ru.md new file mode 100644 index 00000000..f2092fd1 --- /dev/null +++ b/blog/content/edition-2/posts/12-async-await/index.ru.md @@ -0,0 +1,1825 @@ ++++ +title = "Async/Await" +weight = 12 +path = "async-await" +date = 2020-03-27 + +[extra] +chapter = "Multitasking" +# Please update this when updating the translation +translation_based_on_commit = "f2966a53489a1c3eff3e5c3c1c82a3febb47569b" +# GitHub usernames of the people that translated this post +translators = ["TakiMoysha"] + ++++ + +В этом посте мы рассмотрим _кооперативную многозадачность_ и возможности _async/await_ в Rust. Мы подробно рассмотрим, как async/await работает в Rust, включая трейт `Future`, преобразование машины состояний и _pinning_. Затем мы добавим базовую поддержку async/await в наше ядро, by creating an asynchronous keyboard task and a basic executor. + + + +Этот блог открыто разрабатывается на [GitHub]. Если у вас возникают проблемы или вопросы, пожалуйста, откройте issue. Также вы можете оставлять комментарии [внизу][at the bottom]. Исходный код этого поста можно найти в [`post-12` ветку][post branch]. + +[GitHub]: https://github.com/phil-opp/blog_os +[at the bottom]: #comments + +[post branch]: https://github.com/phil-opp/blog_os/tree/post-12 + + + +## Многозадачность + +Одной из основных функций возможностей операционных систем является [_многозадачность_][multitasking], то есть возможность одновременного выполнения нескольких задач. Например, вероятно, пока вы читаете этот пост, у вас открыты другие программы, такие как текстовый редактор или окно терминала. Даже если у вас открыто только одно окно браузера, вероятно, в фоновом режиме выполняются различные задачи по управлению окнами рабочего стола, проверке обновлений или индексированию файлов. + +[_multitasking_]: https://en.wikipedia.org/wiki/Computer_multitasking + +Хотя кажется, что все задачи выполняются параллельно, на одном ядре процессора может выполняться только одна задача за раз. Чтобы создать иллюзию параллельного выполнения задач, операционная система быстро переключается между активными задачами, чтобы каждая из них могла выполнить небольшой прогресс. Поскольку компьютеры работают быстро, мы в большинстве случаев не замечаем этих переключений. + +Когда одноядерные центральные процессоры (ЦП) могут выполнять только одну задачу за раз, многоядерные ЦП могут выполнять несколько задач по настоящему параллельно. Например, процессор с 8 ядрами может выполнять 8 задач одновременно. В следующей статье мы расскажем, как настроить многоядерные ЦП. В этой статье для простоты мы сосредоточимся на одноядерных процессорах. (Стоит отметить, что все многоядерные ЦП запускаются с одним активным ядром, поэтому пока мы можем рассматривать их как одноядерные процессоры). + +Есть две формы многозадачности: _кооперативная_ (совместная) - требует, чтобы задачи регулярно отдавали контроль над процессором для продвижения других задач; _вытесняющая_ (приоритетная) - использующая функционал операционной системы (ОС) для переключения потоков в произвольные моменты моменты времени через принудительную остановку. Далее мы рассмотрим две формы многозадачности более подробно и обсудим их преимущества и недостатки. + +### Вытесняющая Многозадачность + +Идея заключается в том, что ОС контролирует, когда переключать задачи. Для этого она использует факт того, что при каждом прирывании она восстанавливает контрлоль над ЦП. Это позволяет переключать задачи всякий раз, когда в системе появляется новый ввод. Например, возможность переключать задачи когда двигается мышка или приходят пакеты по сети. ОС также может определять точное время, в течении которого задаче разрешается выполняться, настроив аппаратный таймер на отправку прерывания по истечению этого времени. + +На следующем рисунку показан процесс переключения задач при аппаратном прерывании: + +![](regain-control-on-interrupt.svg) + +На первой строке ЦП выполняет задачу `A1` программы `A`. Все другие задачи приостановлены. На второй строке, наступает аппаратное прерывание. Как описанно в посте [_Аппаратные Прерывания_][_Hardware Interrupts_], ЦП немедленно останавливает выполнение задачи `A1` и переходит к обработчику прерываний, определенному в таблице векторов прерываний (Interrupt Descriptor Table, IDT). Благодаря этого обработчику прерывания ОС теперь снова обладает контролем над ЦП, что позволяет ей переключиться на задачу `B1` вместо продолжения задачи `A1`. + +[_Hardware Interrupts_]: @/edition-2/posts/07-hardware-interrupts/index.md + +#### Сохранение состояния + +Поскольку задачи прерываются в произвольные моменты времени, они могут находиться в середине вычислений. Чтобы иметь возможность возобновить их позже, ОС должна создать копию всего состояния задачи, включая ее [стек вызовов][call stack] и значения всех регистров ЦП. Этот процесс называется [_переключением контекста_][_context switch_]. + +[call stack]: https://en.wikipedia.org/wiki/Call_stack +[_context switch_]: https://en.wikipedia.org/wiki/Context_switch + +Поскольку стек вызовов может быть очень большим, операционная система обычно создает отдельный стек вызовов для каждой задачи, вместо того чтобы сохранять содержимое стека вызовов при каждом переключении задач. Такая задача со своим собственным стеком называется [_потоком выполнения_][_thread of execution_] или сокращенно _поток_. Используя отдельный стек для каждой задачи, при переключении контекста необходимо сохранять только содержимое регистров (включая программный счетчик и указатель стека). Такой подход минимизирует накладные расходы на производительность при переключении контекста, что очень важно, поскольку переключения контекста часто происходят до 100 раз в секунду. + +[_thread of execution_]: https://en.wikipedia.org/wiki/Thread_(computing) + +#### Обсуждение + +Основным преимуществом вытесняющей многозадачности является то, что операционная система может полностью контролировать разрешенное время выполнения задачи. Таким образом, она может гарантировать, что каждая задача получит справедливую долю времени процессора, без необходимости полагаться на кооперацию задач. Это особенно важно при выполнении сторонних задач или когда несколько пользователей совместно используют одну систему. + +Недостатком вытесняющей многозадачности является то, что каждой задаче требуется собственный стек. По сравнению с общим стеком это приводит к более высокому использованию памяти на задачу и часто ограничивает количество задач в системе. Другим недостатком является то, что ОС всегда должна сохранять полное состояние регистров ЦП при каждом переключении задач, даже если задача использовала только небольшую часть регистров. + +Вытесняющая многозадачность и потоки - фундаментальные компонтенты ОС, т.к. они позволяют запускать (run untrusted userspace programs ?TODO:). Мы подробнее обсудим эти концепции в будущийх постах. Однако сейчас, мы сосредоточимся на кооперативной многозадачности, которая также предоставляет полезные возможности для нашего ядра. + +### Кооперативная Многозадачность + +Вместо принудительной остановки выполняющихся задач в произвольные моменты времени, кооперативная многозадачность позволяет каждой задаче выполняться до тех пор, пока она добровольно не уступит контроль над ЦП. Это позволяет задачам самостоятельно приостанавливаться в удобные моменты времени, например, когда им нужно ждать операции ввода-вывода. + +Кооперативная многозадачность часто используется на языковом уровне, например в виде [сопрограмм][coroutines] или [async/await]. Идея в том, что программист или компилятор вставляет в программу операции [_yield_], которые отказываются от управления ЦП и позволяют выполняться другим задачам. Например, yield может быть вставлен после каждой итерации сложного цикла. + +[coroutines]: https://en.wikipedia.org/wiki/Coroutine +[async/await]: https://rust-lang.github.io/async-book/01_getting_started/04_async_await_primer.html +[_yield_]: https://en.wikipedia.org/wiki/Yield_(multithreading) + +Часто кооперативную многозадачность совмещают с [асинхронными операциями][asynchronous operations]. Вместо того чтобы ждать завершения операции и препятствовать выполнению других задач в это время, асинхронные операции возвращают статус «не готов», если операция еще не завершена. В этом случае ожидающая задача может выполнить операцию yield, чтобы другие задачи могли выполняться. + +[asynchronous operations]: https://en.wikipedia.org/wiki/Asynchronous_I/O + +#### Сохранение состояния + +Поскольку задачи сами определяют точки паузы, им не нужно, чтобы ОС сохраняла их состояние. Вместо этого они могут сохранять то состояние, которое необходимо для продолжения работы, что часто приводит к улучшению производительности. Например, задаче, которая только что завершила сложные вычисления, может потребоваться только резервное копирование конечного результата вычислений, т.к. промежуточные результаты ей больше не нужны. + +Реализации кооперативных задач, поддерживаемые языком, часто даже могут сохранять необходимые части стека вызовов перед приостановкой. Например, реализация async/await в Rust сохраняет все локальные переменные, которые еще нужны, в автоматически сгенерированной структуре (см. ниже). Благодаря резервному копированию соответствующих частей стека вызовов перед приостановкой все задачи могут использовать один стек вызовов, что приводит к значительному снижению потребления памяти на задачу. Это позволяет создавать практически любое количество кооперативных задач без исчерпания памяти. + +#### Обсуждение + +Недостатком кооперативной многозадачности является то, что некооперативная задача может потенциально выполняться в течение неограниченного времени. Таким образом, вредоносная или содержащая ошибки задача может помешать выполнению других задач и замедлить или даже заблокировать работу всей системы. По этой причине кооперативная многозадачность должна использоваться только в том случае, если известно, что все задачи будут взаимодействовать друг с другом. В качестве противоположного примера можно привести то, что не стоит полагаться на взаимодействие произвольных программ пользовательского уровня в операционной системе. + +Однако высокая производительность и преимущества кооперативной многозадачности в плане памяти делают ее хорошим подходом для использования внутри программы, особенно в сочетании с асинхронными операциями. Поскольку ядро операционной системы является программой, критичной с точки зрения производительности, которая взаимодействует с асинхронным оборудованием, кооперативная многозадачность кажется хорошим подходом для реализации параллелизма. + +## Async/Await в Rust + +Rust предоставляет отличную поддержку кооперативной многозадачности в виде async/await. Прежде чем мы сможем изучить, что такое async/await и как оно работает, нам необходимо понять, как работают _futures_ и асинхронное программирование в Rust. + +### Futures + +_Future_ представляет значение, которое может быть еще недоступно. Это может быть, например, целое число, вычисляемое другой задачей, или файл, загружаемый из сети. Вместо того, чтобы ждать, пока значение станет доступным, futures позволяют продолжить выполнение до тех пор, пока значение не понадобится. + +#### Пример + +Концепцию future лучше всего проиллюстрировать небольшим примером: + +![Диаграмма последовательности: main вызывает `read_file` и блокируется до его возврата; затем вызывает `foo()` и также блокируется до его возврата. Тот же процесс повторяется, но на этот раз вызывается `async_read_file`, который сразу возвращает future; затем снова вызывается `foo()`, который теперь выполняется одновременно с загрузкой файла. Файл становится доступным до возврата `foo()`.](async-example.svg) + +Эта диаграмма последовательности показывает функцию `main`, которая считывает файл из файловой системы, а затем вызывает функцию `foo`. Этот процесс повторяется дважды: один раз с синхронным вызовом `read_file` и один раз с асинхронным вызовом `async_read_file`. + +При синхронном вызове функция `main` должна ждать, пока файл не будет загружен из файловой системы. Только после этого она может вызвать функцию `foo`, которая требует от нее снова ждать результата. + +При асинхронном вызове `async_read_file` файловая система напрямую возвращает будущее значение и загружает файл асинхронно в фоновом режиме. Это позволяет функции `main` вызвать `foo` гораздо раньше, которая затем выполняется параллельно с загрузкой файла. В этом примере загрузка файла даже заканчивается до возврата `foo`, поэтому `main` может напрямую работать с файлом без дальнейшего ожидания после возврата `foo`. + +#### Futures в Rust + +В Rust, futures представленны трейтом [`Future`], который выглядит так: + +[`Future`]: https://doc.rust-lang.org/nightly/core/future/trait.Future.html + +```rust +pub trait Future { + type Output; + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll; +} +``` + +[Ассоциированный тип][associated type] `Output` определяет тип асинхронного значения. Например, функция `async_read_file` на приведенной выше диаграмме вернет экземпляр `Future` с `Output`, установленным как `File`. + +[associated type]: https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#specifying-placeholder-types-in-trait-definitions-with-associated-types + +Метод [`poll`] позволяет проверить, доступно ли значение. Он возвращает перечисление [`Poll`], которое выглядит следующим образом: + +[`poll`]: https://doc.rust-lang.org/nightly/core/future/trait.Future.html#tymethod.poll +[`Poll`]: https://doc.rust-lang.org/nightly/core/task/enum.Poll.html + +```rust +pub enum Poll { + Ready(T), + Pending, +} +``` + +Когда значение уже доступно (например, файл был полностью прочитан с диска), оно возвращается, обернутое в вариант `Ready`. Иначе возвращается вариант `Pending`, который сигнализирует вызывающему, что значение еще не доступно. + +Метод `poll` принимает два аргумента: `self: Pin<&mut Self>` и `cx: &mut Context`. Первый аргумент ведет себя аналогично обычной ссылке `&mut self`, за исключением того, что значение `Self` [_pinned_] к своему месту в памяти. Понять `Pin` и его необходимость сложно, не понимая сначала, как работает async/await. Поэтому мы объясним это позже в этом посте. + +[_pinned_]: https://doc.rust-lang.org/nightly/core/pin/index.html + +Параметр `cx: &mut Context` нужен для передачи экземпляра [`Waker`] в асинхронную задачу, например, загрузку файловой системы. Этот `Waker` позволяет асинхронной задаче сообщать о том, что она (или ее часть) завершена, например, что файл был загружен с диска. Поскольку основная задача знает, что она будет уведомлена, когда `Future` будет готов, ей не нужно повторно вызывать `poll`. Мы объясним этот процесс более подробно позже в этом посте, когда будем реализовывать наш собственный тип waker. + +[`Waker`]: https://doc.rust-lang.org/nightly/core/task/struct.Waker.html + +### Работа с Futures + +Теперь мы знаем, как определяются футуры, и понимаем основную идею метода `poll`. Однако мы все еще не знаем, как эффективно работать с футурами. Проблема в том, что они представляют собой результаты асинхронных задач, которые могут быть еще недоступны. На практике, однако, нам часто нужны эти значения непосредственно для дальнейших вычислений. Поэтому возникает вопрос: как мы можем эффективно получить значение, когда оно нам нужно? + +#### Ожидание Futures + +Один из возможных ответов — дождаться, пока будущее станет реальностью. Это может выглядеть примерно так: + +```rust +let future = async_read_file("foo.txt"); +let file_content = loop { + match future.poll(…) { + Poll::Ready(value) => break value, + Poll::Pending => {}, // ничего не делать + } +} +``` + +Здесь мы _активно_ ждем футуру, вызывая `poll` снова и снова в цикле. Аргументы `poll` опущены, т.к. здесь они не имеют значения. Хотя это решение работает, оно очень неэффективно, потому что мы занимаем CPU до тех пор, пока значение не станет доступным. + +Более эффективным подходом может быть _блокировка_ текущего потока до тех пор, пока футура не станет доступной. Конечно, это возможно только при наличии потоков, поэтому это решение не работает для нашего ядра, по крайней мере, пока. Даже в системах, где поддерживается блокировка, она часто нежелательна, поскольку превращает асинхронную задачу в синхронную, тем самым сдерживая потенциальные преимущества параллельных задач в плане производительности. + + +#### Future Combinators + +An alternative to waiting is to use future combinators. Future combinators are methods like `map` that allow chaining and combining futures together, similar to the methods of the [`Iterator`] trait. Instead of waiting on the future, these combinators return a future themselves, which applies the mapping operation on `poll`. + +[`Iterator`]: https://doc.rust-lang.org/stable/core/iter/trait.Iterator.html + +As an example, a simple `string_len` combinator for converting a `Future` to a `Future` could look like this: + +```rust +struct StringLen { + inner_future: F, +} + +impl Future for StringLen where F: Future { + type Output = usize; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.inner_future.poll(cx) { + Poll::Ready(s) => Poll::Ready(s.len()), + Poll::Pending => Poll::Pending, + } + } +} + +fn string_len(string: impl Future) + -> impl Future +{ + StringLen { + inner_future: string, + } +} + +// Usage +fn file_len() -> impl Future { + let file_content_future = async_read_file("foo.txt"); + string_len(file_content_future) +} +``` + +This code does not quite work because it does not handle [_pinning_], but it suffices as an example. The basic idea is that the `string_len` function wraps a given `Future` instance into a new `StringLen` struct, which also implements `Future`. When the wrapped future is polled, it polls the inner future. If the value is not ready yet, `Poll::Pending` is returned from the wrapped future too. If the value is ready, the string is extracted from the `Poll::Ready` variant and its length is calculated. Afterwards, it is wrapped in `Poll::Ready` again and returned. + +[_pinning_]: https://doc.rust-lang.org/stable/core/pin/index.html + +With this `string_len` function, we can calculate the length of an asynchronous string without waiting for it. Since the function returns a `Future` again, the caller can't work directly on the returned value, but needs to use combinator functions again. This way, the whole call graph becomes asynchronous and we can efficiently wait for multiple futures at once at some point, e.g., in the main function. + +Because manually writing combinator functions is difficult, they are often provided by libraries. While the Rust standard library itself provides no combinator methods yet, the semi-official (and `no_std` compatible) [`futures`] crate does. Its [`FutureExt`] trait provides high-level combinator methods such as [`map`] or [`then`], which can be used to manipulate the result with arbitrary closures. + +[`futures`]: https://docs.rs/futures/0.3.4/futures/ +[`FutureExt`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html +[`map`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html#method.map +[`then`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html#method.then + +##### Advantages + +The big advantage of future combinators is that they keep the operations asynchronous. In combination with asynchronous I/O interfaces, this approach can lead to very high performance. The fact that future combinators are implemented as normal structs with trait implementations allows the compiler to excessively optimize them. For more details, see the [_Zero-cost futures in Rust_] post, which announced the addition of futures to the Rust ecosystem. + +[_Zero-cost futures in Rust_]: https://aturon.github.io/blog/2016/08/11/futures/ + +##### Drawbacks + +While future combinators make it possible to write very efficient code, they can be difficult to use in some situations because of the type system and the closure-based interface. For example, consider code like this: + +```rust +fn example(min_len: usize) -> impl Future { + async_read_file("foo.txt").then(move |content| { + if content.len() < min_len { + Either::Left(async_read_file("bar.txt").map(|s| content + &s)) + } else { + Either::Right(future::ready(content)) + } + }) +} +``` + +([Try it on the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=91fc09024eecb2448a85a7ef6a97b8d8)) + +Here we read the file `foo.txt` and then use the [`then`] combinator to chain a second future based on the file content. If the content length is smaller than the given `min_len`, we read a different `bar.txt` file and append it to `content` using the [`map`] combinator. Otherwise, we return only the content of `foo.txt`. + +We need to use the [`move` keyword] for the closure passed to `then` because otherwise there would be a lifetime error for `min_len`. The reason for the [`Either`] wrapper is that `if` and `else` blocks must always have the same type. Since we return different future types in the blocks, we must use the wrapper type to unify them into a single type. The [`ready`] function wraps a value into a future, which is immediately ready. The function is required here because the `Either` wrapper expects that the wrapped value implements `Future`. + +[`move` keyword]: https://doc.rust-lang.org/std/keyword.move.html +[`Either`]: https://docs.rs/futures/0.3.4/futures/future/enum.Either.html +[`ready`]: https://docs.rs/futures/0.3.4/futures/future/fn.ready.html + +As you can imagine, this can quickly lead to very complex code for larger projects. It gets especially complicated if borrowing and different lifetimes are involved. For this reason, a lot of work was invested in adding support for async/await to Rust, with the goal of making asynchronous code radically simpler to write. + +### The Async/Await Pattern + +The idea behind async/await is to let the programmer write code that _looks_ like normal synchronous code, but is turned into asynchronous code by the compiler. It works based on the two keywords `async` and `await`. The `async` keyword can be used in a function signature to turn a synchronous function into an asynchronous function that returns a future: + +```rust +async fn foo() -> u32 { + 0 +} + +// the above is roughly translated by the compiler to: +fn foo() -> impl Future { + future::ready(0) +} +``` + +This keyword alone wouldn't be that useful. However, inside `async` functions, the `await` keyword can be used to retrieve the asynchronous value of a future: + +```rust +async fn example(min_len: usize) -> String { + let content = async_read_file("foo.txt").await; + if content.len() < min_len { + content + &async_read_file("bar.txt").await + } else { + content + } +} +``` + +([Try it on the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d93c28509a1c67661f31ff820281d434)) + +This function is a direct translation of the `example` function from [above](#drawbacks) that used combinator functions. Using the `.await` operator, we can retrieve the value of a future without needing any closures or `Either` types. As a result, we can write our code like we write normal synchronous code, with the difference that _this is still asynchronous code_. + +#### State Machine Transformation + +Behind the scenes, the compiler converts the body of the `async` function into a [_state machine_], with each `.await` call representing a different state. For the above `example` function, the compiler creates a state machine with the following four states: + +[_state machine_]: https://en.wikipedia.org/wiki/Finite-state_machine + +![Four states: start, waiting on foo.txt, waiting on bar.txt, end](async-state-machine-states.svg) + +Each state represents a different pause point in the function. The _"Start"_ and _"End"_ states represent the function at the beginning and end of its execution. The _"Waiting on foo.txt"_ state represents that the function is currently waiting for the first `async_read_file` result. Similarly, the _"Waiting on bar.txt"_ state represents the pause point where the function is waiting on the second `async_read_file` result. + +The state machine implements the `Future` trait by making each `poll` call a possible state transition: + +![Four states and their transitions: start, waiting on foo.txt, waiting on bar.txt, end](async-state-machine-basic.svg) + +The diagram uses arrows to represent state switches and diamond shapes to represent alternative ways. For example, if the `foo.txt` file is not ready, the path marked with _"no"_ is taken and the _"Waiting on foo.txt"_ state is reached. Otherwise, the _"yes"_ path is taken. The small red diamond without a caption represents the `if content.len() < 100` branch of the `example` function. + +We see that the first `poll` call starts the function and lets it run until it reaches a future that is not ready yet. If all futures on the path are ready, the function can run till the _"End"_ state, where it returns its result wrapped in `Poll::Ready`. Otherwise, the state machine enters a waiting state and returns `Poll::Pending`. On the next `poll` call, the state machine then starts from the last waiting state and retries the last operation. + +#### Saving State + +In order to be able to continue from the last waiting state, the state machine must keep track of the current state internally. In addition, it must save all the variables that it needs to continue execution on the next `poll` call. This is where the compiler can really shine: Since it knows which variables are used when, it can automatically generate structs with exactly the variables that are needed. + +As an example, the compiler generates structs like the following for the above `example` function: + +```rust +// The `example` function again so that you don't have to scroll up +async fn example(min_len: usize) -> String { + let content = async_read_file("foo.txt").await; + if content.len() < min_len { + content + &async_read_file("bar.txt").await + } else { + content + } +} + +// The compiler-generated state structs: + +struct StartState { + min_len: usize, +} + +struct WaitingOnFooTxtState { + min_len: usize, + foo_txt_future: impl Future, +} + +struct WaitingOnBarTxtState { + content: String, + bar_txt_future: impl Future, +} + +struct EndState {} +``` + +In the "start" and _"Waiting on foo.txt"_ states, the `min_len` parameter needs to be stored for the later comparison with `content.len()`. The _"Waiting on foo.txt"_ state additionally stores a `foo_txt_future`, which represents the future returned by the `async_read_file` call. This future needs to be polled again when the state machine continues, so it needs to be saved. + +The _"Waiting on bar.txt"_ state contains the `content` variable for the later string concatenation when `bar.txt` is ready. It also stores a `bar_txt_future` that represents the in-progress load of `bar.txt`. The struct does not contain the `min_len` variable because it is no longer needed after the `content.len()` comparison. In the _"end"_ state, no variables are stored because the function has already run to completion. + +Keep in mind that this is only an example of the code that the compiler could generate. The struct names and the field layout are implementation details and might be different. + +#### The Full State Machine Type + +While the exact compiler-generated code is an implementation detail, it helps in understanding to imagine how the generated state machine _could_ look for the `example` function. We already defined the structs representing the different states and containing the required variables. To create a state machine on top of them, we can combine them into an [`enum`]: + +[`enum`]: https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html + +```rust +enum ExampleStateMachine { + Start(StartState), + WaitingOnFooTxt(WaitingOnFooTxtState), + WaitingOnBarTxt(WaitingOnBarTxtState), + End(EndState), +} +``` + +We define a separate enum variant for each state and add the corresponding state struct to each variant as a field. To implement the state transitions, the compiler generates an implementation of the `Future` trait based on the `example` function: + +```rust +impl Future for ExampleStateMachine { + type Output = String; // return type of `example` + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + loop { + match self { // TODO: handle pinning + ExampleStateMachine::Start(state) => {…} + ExampleStateMachine::WaitingOnFooTxt(state) => {…} + ExampleStateMachine::WaitingOnBarTxt(state) => {…} + ExampleStateMachine::End(state) => {…} + } + } + } +} +``` + +The `Output` type of the future is `String` because it's the return type of the `example` function. To implement the `poll` function, we use a `match` statement on the current state inside a `loop`. The idea is that we switch to the next state as long as possible and use an explicit `return Poll::Pending` when we can't continue. + +For simplicity, we only show simplified code and don't handle [pinning][_pinning_], ownership, lifetimes, etc. So this and the following code should be treated as pseudo-code and not used directly. Of course, the real compiler-generated code handles everything correctly, albeit possibly in a different way. + +To keep the code excerpts small, we present the code for each `match` arm separately. Let's begin with the `Start` state: + +```rust +ExampleStateMachine::Start(state) => { + // from body of `example` + let foo_txt_future = async_read_file("foo.txt"); + // `.await` operation + let state = WaitingOnFooTxtState { + min_len: state.min_len, + foo_txt_future, + }; + *self = ExampleStateMachine::WaitingOnFooTxt(state); +} +``` + +The state machine is in the `Start` state when it is right at the beginning of the function. In this case, we execute all the code from the body of the `example` function until the first `.await`. To handle the `.await` operation, we change the state of the `self` state machine to `WaitingOnFooTxt`, which includes the construction of the `WaitingOnFooTxtState` struct. + +Since the `match self {…}` statement is executed in a loop, the execution jumps to the `WaitingOnFooTxt` arm next: + +```rust +ExampleStateMachine::WaitingOnFooTxt(state) => { + match state.foo_txt_future.poll(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(content) => { + // from body of `example` + if content.len() < state.min_len { + let bar_txt_future = async_read_file("bar.txt"); + // `.await` operation + let state = WaitingOnBarTxtState { + content, + bar_txt_future, + }; + *self = ExampleStateMachine::WaitingOnBarTxt(state); + } else { + *self = ExampleStateMachine::End(EndState); + return Poll::Ready(content); + } + } + } +} +``` + +In this `match` arm, we first call the `poll` function of the `foo_txt_future`. If it is not ready, we exit the loop and return `Poll::Pending`. Since `self` stays in the `WaitingOnFooTxt` state in this case, the next `poll` call on the state machine will enter the same `match` arm and retry polling the `foo_txt_future`. + +When the `foo_txt_future` is ready, we assign the result to the `content` variable and continue to execute the code of the `example` function: If `content.len()` is smaller than the `min_len` saved in the state struct, the `bar.txt` file is read asynchronously. We again translate the `.await` operation into a state change, this time into the `WaitingOnBarTxt` state. Since we're executing the `match` inside a loop, the execution directly jumps to the `match` arm for the new state afterward, where the `bar_txt_future` is polled. + +In case we enter the `else` branch, no further `.await` operation occurs. We reach the end of the function and return `content` wrapped in `Poll::Ready`. We also change the current state to the `End` state. + +The code for the `WaitingOnBarTxt` state looks like this: + +```rust +ExampleStateMachine::WaitingOnBarTxt(state) => { + match state.bar_txt_future.poll(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(bar_txt) => { + *self = ExampleStateMachine::End(EndState); + // from body of `example` + return Poll::Ready(state.content + &bar_txt); + } + } +} +``` + +Similar to the `WaitingOnFooTxt` state, we start by polling the `bar_txt_future`. If it is still pending, we exit the loop and return `Poll::Pending`. Otherwise, we can perform the last operation of the `example` function: concatenating the `content` variable with the result from the future. We update the state machine to the `End` state and then return the result wrapped in `Poll::Ready`. + +Finally, the code for the `End` state looks like this: + +```rust +ExampleStateMachine::End(_) => { + panic!("poll called after Poll::Ready was returned"); +} +``` + +Futures should not be polled again after they returned `Poll::Ready`, so we panic if `poll` is called while we are already in the `End` state. + +We now know what the compiler-generated state machine and its implementation of the `Future` trait _could_ look like. In practice, the compiler generates code in a different way. (In case you're interested, the implementation is currently based on [_coroutines_], but this is only an implementation detail.) + +[_coroutines_]: https://doc.rust-lang.org/stable/unstable-book/language-features/coroutines.html + +The last piece of the puzzle is the generated code for the `example` function itself. Remember, the function header was defined like this: + +```rust +async fn example(min_len: usize) -> String +``` + +Since the complete function body is now implemented by the state machine, the only thing that the function needs to do is to initialize the state machine and return it. The generated code for this could look like this: + +```rust +fn example(min_len: usize) -> ExampleStateMachine { + ExampleStateMachine::Start(StartState { + min_len, + }) +} +``` + +The function no longer has an `async` modifier since it now explicitly returns an `ExampleStateMachine` type, which implements the `Future` trait. As expected, the state machine is constructed in the `Start` state and the corresponding state struct is initialized with the `min_len` parameter. + +Note that this function does not start the execution of the state machine. This is a fundamental design decision of futures in Rust: they do nothing until they are polled for the first time. + +### Pinning + +We already stumbled across _pinning_ multiple times in this post. Now is finally the time to explore what pinning is and why it is needed. + +#### Self-Referential Structs + +As explained above, the state machine transformation stores the local variables of each pause point in a struct. For small examples like our `example` function, this was straightforward and did not lead to any problems. However, things become more difficult when variables reference each other. For example, consider this function: + +```rust +async fn pin_example() -> i32 { + let array = [1, 2, 3]; + let element = &array[2]; + async_write_file("foo.txt", element.to_string()).await; + *element +} +``` + +This function creates a small `array` with the contents `1`, `2`, and `3`. It then creates a reference to the last array element and stores it in an `element` variable. Next, it asynchronously writes the number converted to a string to a `foo.txt` file. Finally, it returns the number referenced by `element`. + +Since the function uses a single `await` operation, the resulting state machine has three states: start, end, and "waiting on write". The function takes no arguments, so the struct for the start state is empty. Like before, the struct for the end state is empty because the function is finished at this point. The struct for the "waiting on write" state is more interesting: + +```rust +struct WaitingOnWriteState { + array: [1, 2, 3], + element: 0x1001c, // address of the last array element +} +``` + +We need to store both the `array` and `element` variables because `element` is required for the return value and `array` is referenced by `element`. Since `element` is a reference, it stores a _pointer_ (i.e., a memory address) to the referenced element. We used `0x1001c` as an example memory address here. In reality, it needs to be the address of the last element of the `array` field, so it depends on where the struct lives in memory. Structs with such internal pointers are called _self-referential_ structs because they reference themselves from one of their fields. + +#### The Problem with Self-Referential Structs + +The internal pointer of our self-referential struct leads to a fundamental problem, which becomes apparent when we look at its memory layout: + +![array at 0x10014 with fields 1, 2, and 3; element at address 0x10020, pointing to the last array element at 0x1001c](self-referential-struct.svg) + +The `array` field starts at address 0x10014 and the `element` field at address 0x10020. It points to address 0x1001c because the last array element lives at this address. At this point, everything is still fine. However, an issue occurs when we move this struct to a different memory address: + +![array at 0x10024 with fields 1, 2, and 3; element at address 0x10030, still pointing to 0x1001c, even though the last array element now lives at 0x1002c](self-referential-struct-moved.svg) + +We moved the struct a bit so that it starts at address `0x10024` now. This could, for example, happen when we pass the struct as a function argument or assign it to a different stack variable. The problem is that the `element` field still points to address `0x1001c` even though the last `array` element now lives at address `0x1002c`. Thus, the pointer is dangling, with the result that undefined behavior occurs on the next `poll` call. + +#### Possible Solutions + +There are three fundamental approaches to solving the dangling pointer problem: + +- **Update the pointer on move:** The idea is to update the internal pointer whenever the struct is moved in memory so that it is still valid after the move. Unfortunately, this approach would require extensive changes to Rust that would result in potentially huge performance losses. The reason is that some kind of runtime would need to keep track of the type of all struct fields and check on every move operation whether a pointer update is required. +- **Store an offset instead of self-references:**: To avoid the requirement for updating pointers, the compiler could try to store self-references as offsets from the struct's beginning instead. For example, the `element` field of the above `WaitingOnWriteState` struct could be stored in the form of an `element_offset` field with a value of 8 because the array element that the reference points to starts 8 bytes after the struct's beginning. Since the offset stays the same when the struct is moved, no field updates are required. + + The problem with this approach is that it requires the compiler to detect all self-references. This is not possible at compile-time because the value of a reference might depend on user input, so we would need a runtime system again to analyze references and correctly create the state structs. This would not only result in runtime costs but also prevent certain compiler optimizations, so that it would cause large performance losses again. +- **Forbid moving the struct:** As we saw above, the dangling pointer only occurs when we move the struct in memory. By completely forbidding move operations on self-referential structs, the problem can also be avoided. The big advantage of this approach is that it can be implemented at the type system level without additional runtime costs. The drawback is that it puts the burden of dealing with move operations on possibly self-referential structs on the programmer. + +Rust chose the third solution because of its principle of providing _zero cost abstractions_, which means that abstractions should not impose additional runtime costs. The [_pinning_] API was proposed for this purpose in [RFC 2349](https://github.com/rust-lang/rfcs/blob/master/text/2349-pin.md). In the following, we will give a short overview of this API and explain how it works with async/await and futures. + +#### Heap Values + +The first observation is that [heap-allocated] values already have a fixed memory address most of the time. They are created using a call to `allocate` and then referenced by a pointer type such as `Box`. While moving the pointer type is possible, the heap value that the pointer points to stays at the same memory address until it is freed through a `deallocate` call again. + +[heap-allocated]: @/edition-2/posts/10-heap-allocation/index.md + +Using heap allocation, we can try to create a self-referential struct: + +```rust +fn main() { + let mut heap_value = Box::new(SelfReferential { + self_ptr: 0 as *const _, + }); + let ptr = &*heap_value as *const SelfReferential; + heap_value.self_ptr = ptr; + println!("heap value at: {:p}", heap_value); + println!("internal reference: {:p}", heap_value.self_ptr); +} + +struct SelfReferential { + self_ptr: *const Self, +} +``` + +([Try it on the playground][playground-self-ref]) + +[playground-self-ref]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ce1aff3a37fcc1c8188eeaf0f39c97e8 + +We create a simple struct named `SelfReferential` that contains a single pointer field. First, we initialize this struct with a null pointer and then allocate it on the heap using `Box::new`. We then determine the memory address of the heap-allocated struct and store it in a `ptr` variable. Finally, we make the struct self-referential by assigning the `ptr` variable to the `self_ptr` field. + +When we execute this code [on the playground][playground-self-ref], we see that the address of the heap value and its internal pointer are equal, which means that the `self_ptr` field is a valid self-reference. Since the `heap_value` variable is only a pointer, moving it (e.g., by passing it to a function) does not change the address of the struct itself, so the `self_ptr` stays valid even if the pointer is moved. + +However, there is still a way to break this example: We can move out of a `Box` or replace its content: + +```rust +let stack_value = mem::replace(&mut *heap_value, SelfReferential { + self_ptr: 0 as *const _, +}); +println!("value at: {:p}", &stack_value); +println!("internal reference: {:p}", stack_value.self_ptr); +``` + +([Try it on the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e160ee8a64cba4cebc1c0473dcecb7c8)) + +Here we use the [`mem::replace`] function to replace the heap-allocated value with a new struct instance. This allows us to move the original `heap_value` to the stack, while the `self_ptr` field of the struct is now a dangling pointer that still points to the old heap address. When you try to run the example on the playground, you see that the printed _"value at:"_ and _"internal reference:"_ lines indeed show different pointers. So heap allocating a value is not enough to make self-references safe. + +[`mem::replace`]: https://doc.rust-lang.org/nightly/core/mem/fn.replace.html + +The fundamental problem that allowed the above breakage is that `Box` allows us to get a `&mut T` reference to the heap-allocated value. This `&mut` reference makes it possible to use methods like [`mem::replace`] or [`mem::swap`] to invalidate the heap-allocated value. To resolve this problem, we must prevent `&mut` references to self-referential structs from being created. + +[`mem::swap`]: https://doc.rust-lang.org/nightly/core/mem/fn.swap.html + +#### `Pin>` and `Unpin` + +The pinning API provides a solution to the `&mut T` problem in the form of the [`Pin`] wrapper type and the [`Unpin`] marker trait. The idea behind these types is to gate all methods of `Pin` that can be used to get `&mut` references to the wrapped value (e.g. [`get_mut`][pin-get-mut] or [`deref_mut`][pin-deref-mut]) on the `Unpin` trait. The `Unpin` trait is an [_auto trait_], which is automatically implemented for all types except those that explicitly opt-out. By making self-referential structs opt-out of `Unpin`, there is no (safe) way to get a `&mut T` from a `Pin>` type for them. As a result, their internal self-references are guaranteed to stay valid. + +[`Pin`]: https://doc.rust-lang.org/stable/core/pin/struct.Pin.html +[`Unpin`]: https://doc.rust-lang.org/nightly/std/marker/trait.Unpin.html +[pin-get-mut]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.get_mut +[pin-deref-mut]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.deref_mut +[_auto trait_]: https://doc.rust-lang.org/reference/special-types-and-traits.html#auto-traits + +As an example, let's update the `SelfReferential` type from above to opt-out of `Unpin`: + +```rust +use core::marker::PhantomPinned; + +struct SelfReferential { + self_ptr: *const Self, + _pin: PhantomPinned, +} +``` + +We opt-out by adding a second `_pin` field of type [`PhantomPinned`]. This type is a zero-sized marker type whose only purpose is to _not_ implement the `Unpin` trait. Because of the way [auto traits][_auto trait_] work, a single field that is not `Unpin` suffices to make the complete struct opt-out of `Unpin`. + +[`PhantomPinned`]: https://doc.rust-lang.org/nightly/core/marker/struct.PhantomPinned.html + +The second step is to change the `Box` type in the example to a `Pin>` type. The easiest way to do this is to use the [`Box::pin`] function instead of [`Box::new`] for creating the heap-allocated value: + +[`Box::pin`]: https://doc.rust-lang.org/nightly/alloc/boxed/struct.Box.html#method.pin +[`Box::new`]: https://doc.rust-lang.org/nightly/alloc/boxed/struct.Box.html#method.new + +```rust +let mut heap_value = Box::pin(SelfReferential { + self_ptr: 0 as *const _, + _pin: PhantomPinned, +}); +``` + +In addition to changing `Box::new` to `Box::pin`, we also need to add the new `_pin` field in the struct initializer. Since `PhantomPinned` is a zero-sized type, we only need its type name to initialize it. + +When we [try to run our adjusted example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=961b0db194bbe851ff4d0ed08d3bd98a) now, we see that it no longer works: + +``` +error[E0594]: cannot assign to data in a dereference of `std::pin::Pin>` + --> src/main.rs:10:5 + | +10 | heap_value.self_ptr = ptr; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ cannot assign + | + = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `std::pin::Pin>` + +error[E0596]: cannot borrow data in a dereference of `std::pin::Pin>` as mutable + --> src/main.rs:16:36 + | +16 | let stack_value = mem::replace(&mut *heap_value, SelfReferential { + | ^^^^^^^^^^^^^^^^ cannot borrow as mutable + | + = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `std::pin::Pin>` +``` + +Both errors occur because the `Pin>` type no longer implements the `DerefMut` trait. This is exactly what we wanted because the `DerefMut` trait would return a `&mut` reference, which we wanted to prevent. This only happens because we both opted-out of `Unpin` and changed `Box::new` to `Box::pin`. + +The problem now is that the compiler does not only prevent moving the type in line 16, but also forbids initializing the `self_ptr` field in line 10. This happens because the compiler can't differentiate between valid and invalid uses of `&mut` references. To get the initialization working again, we have to use the unsafe [`get_unchecked_mut`] method: + +[`get_unchecked_mut`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.get_unchecked_mut + +```rust +// safe because modifying a field doesn't move the whole struct +unsafe { + let mut_ref = Pin::as_mut(&mut heap_value); + Pin::get_unchecked_mut(mut_ref).self_ptr = ptr; +} +``` + +([Try it on the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b9ebbb11429d9d79b3f9fffe819e2018)) + +The [`get_unchecked_mut`] function works on a `Pin<&mut T>` instead of a `Pin>`, so we have to use [`Pin::as_mut`] for converting the value. Then we can set the `self_ptr` field using the `&mut` reference returned by `get_unchecked_mut`. + +[`Pin::as_mut`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.as_mut + +Now the only error left is the desired error on `mem::replace`. Remember, this operation tries to move the heap-allocated value to the stack, which would break the self-reference stored in the `self_ptr` field. By opting out of `Unpin` and using `Pin>`, we can prevent this operation at compile time and thus safely work with self-referential structs. As we saw, the compiler is not able to prove that the creation of the self-reference is safe (yet), so we need to use an unsafe block and verify the correctness ourselves. + +#### Stack Pinning and `Pin<&mut T>` + +In the previous section, we learned how to use `Pin>` to safely create a heap-allocated self-referential value. While this approach works fine and is relatively safe (apart from the unsafe construction), the required heap allocation comes with a performance cost. Since Rust strives to provide _zero-cost abstractions_ whenever possible, the pinning API also allows to create `Pin<&mut T>` instances that point to stack-allocated values. + +Unlike `Pin>` instances, which have _ownership_ of the wrapped value, `Pin<&mut T>` instances only temporarily borrow the wrapped value. This makes things more complicated, as it requires the programmer to ensure additional guarantees themselves. Most importantly, a `Pin<&mut T>` must stay pinned for the whole lifetime of the referenced `T`, which can be difficult to verify for stack-based variables. To help with this, crates like [`pin-utils`] exist, but I still wouldn't recommend pinning to the stack unless you really know what you're doing. + +[`pin-utils`]: https://docs.rs/pin-utils/0.1.0-alpha.4/pin_utils/ + +For further reading, check out the documentation of the [`pin` module] and the [`Pin::new_unchecked`] method. + +[`pin` module]: https://doc.rust-lang.org/nightly/core/pin/index.html +[`Pin::new_unchecked`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.new_unchecked + +#### Pinning and Futures + +As we already saw in this post, the [`Future::poll`] method uses pinning in the form of a `Pin<&mut Self>` parameter: + +[`Future::poll`]: https://doc.rust-lang.org/nightly/core/future/trait.Future.html#tymethod.poll + +```rust +fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll +``` + +The reason that this method takes `self: Pin<&mut Self>` instead of the normal `&mut self` is that future instances created from async/await are often self-referential, as we saw [above][self-ref-async-await]. By wrapping `Self` into `Pin` and letting the compiler opt-out of `Unpin` for self-referential futures generated from async/await, it is guaranteed that the futures are not moved in memory between `poll` calls. This ensures that all internal references are still valid. + +[self-ref-async-await]: @/edition-2/posts/12-async-await/index.md#self-referential-structs + +It is worth noting that moving futures before the first `poll` call is fine. This is a result of the fact that futures are lazy and do nothing until they're polled for the first time. The `start` state of the generated state machines therefore only contains the function arguments but no internal references. In order to call `poll`, the caller must wrap the future into `Pin` first, which ensures that the future cannot be moved in memory anymore. Since stack pinning is more difficult to get right, I recommend to always use [`Box::pin`] combined with [`Pin::as_mut`] for this. + +[`futures`]: https://docs.rs/futures/0.3.4/futures/ + +In case you're interested in understanding how to safely implement a future combinator function using stack pinning yourself, take a look at the relatively short [source of the `map` combinator method][map-src] of the `futures` crate and the section about [projections and structural pinning] of the pin documentation. + +[map-src]: https://docs.rs/futures-util/0.3.4/src/futures_util/future/future/map.rs.html +[projections and structural pinning]: https://doc.rust-lang.org/stable/std/pin/index.html#projections-and-structural-pinning + +### Executors and Wakers + +Using async/await, it is possible to ergonomically work with futures in a completely asynchronous way. However, as we learned above, futures do nothing until they are polled. This means we have to call `poll` on them at some point, otherwise the asynchronous code is never executed. + +With a single future, we can always wait for each future manually using a loop [as described above](#waiting-on-futures). However, this approach is very inefficient and not practical for programs that create a large number of futures. The most common solution to this problem is to define a global _executor_ that is responsible for polling all futures in the system until they are finished. + +#### Executors + +The purpose of an executor is to allow spawning futures as independent tasks, typically through some sort of `spawn` method. The executor is then responsible for polling all futures until they are completed. The big advantage of managing all futures in a central place is that the executor can switch to a different future whenever a future returns `Poll::Pending`. Thus, asynchronous operations are run in parallel and the CPU is kept busy. + +Many executor implementations can also take advantage of systems with multiple CPU cores. They create a [thread pool] that is able to utilize all cores if there is enough work available and use techniques such as [work stealing] to balance the load between cores. There are also special executor implementations for embedded systems that optimize for low latency and memory overhead. + +[thread pool]: https://en.wikipedia.org/wiki/Thread_pool +[work stealing]: https://en.wikipedia.org/wiki/Work_stealing + +To avoid the overhead of polling futures repeatedly, executors typically take advantage of the _waker_ API supported by Rust's futures. + +#### Wakers + +The idea behind the waker API is that a special [`Waker`] type is passed to each invocation of `poll`, wrapped in the [`Context`] type. This `Waker` type is created by the executor and can be used by the asynchronous task to signal its (partial) completion. As a result, the executor does not need to call `poll` on a future that previously returned `Poll::Pending` until it is notified by the corresponding waker. + +[`Context`]: https://doc.rust-lang.org/nightly/core/task/struct.Context.html + +This is best illustrated by a small example: + +```rust +async fn write_file() { + async_write_file("foo.txt", "Hello").await; +} +``` + +This function asynchronously writes the string "Hello" to a `foo.txt` file. Since hard disk writes take some time, the first `poll` call on this future will likely return `Poll::Pending`. However, the hard disk driver will internally store the `Waker` passed to the `poll` call and use it to notify the executor when the file is written to disk. This way, the executor does not need to waste any time trying to `poll` the future again before it receives the waker notification. + +We will see how the `Waker` type works in detail when we create our own executor with waker support in the implementation section of this post. + +### Cooperative Multitasking? + +At the beginning of this post, we talked about preemptive and cooperative multitasking. While preemptive multitasking relies on the operating system to forcibly switch between running tasks, cooperative multitasking requires that the tasks voluntarily give up control of the CPU through a _yield_ operation on a regular basis. The big advantage of the cooperative approach is that tasks can save their state themselves, which results in more efficient context switches and makes it possible to share the same call stack between tasks. + +It might not be immediately apparent, but futures and async/await are an implementation of the cooperative multitasking pattern: + +- Each future that is added to the executor is basically a cooperative task. +- Instead of using an explicit yield operation, futures give up control of the CPU core by returning `Poll::Pending` (or `Poll::Ready` at the end). + - There is nothing that forces futures to give up the CPU. If they want, they can never return from `poll`, e.g., by spinning endlessly in a loop. + - Since each future can block the execution of the other futures in the executor, we need to trust them to not be malicious. +- Futures internally store all the state they need to continue execution on the next `poll` call. With async/await, the compiler automatically detects all variables that are needed and stores them inside the generated state machine. + - Only the minimum state required for continuation is saved. + - Since the `poll` method gives up the call stack when it returns, the same stack can be used for polling other futures. + +We see that futures and async/await fit the cooperative multitasking pattern perfectly; they just use some different terminology. In the following, we will therefore use the terms "task" and "future" interchangeably. + +## Implementation + +Now that we understand how cooperative multitasking based on futures and async/await works in Rust, it's time to add support for it to our kernel. Since the [`Future`] trait is part of the `core` library and async/await is a feature of the language itself, there is nothing special we need to do to use it in our `#![no_std]` kernel. The only requirement is that we use at least nightly `2020-03-25` of Rust because async/await was not `no_std` compatible before. + +With a recent-enough nightly, we can start using async/await in our `main.rs`: + +```rust +// in src/main.rs + +async fn async_number() -> u32 { + 42 +} + +async fn example_task() { + let number = async_number().await; + println!("async number: {}", number); +} +``` + +The `async_number` function is an `async fn`, so the compiler transforms it into a state machine that implements `Future`. Since the function only returns `42`, the resulting future will directly return `Poll::Ready(42)` on the first `poll` call. Like `async_number`, the `example_task` function is also an `async fn`. It awaits the number returned by `async_number` and then prints it using the `println` macro. + +To run the future returned by `example_task`, we need to call `poll` on it until it signals its completion by returning `Poll::Ready`. To do this, we need to create a simple executor type. + +### Task + +Before we start the executor implementation, we create a new `task` module with a `Task` type: + +```rust +// in src/lib.rs + +pub mod task; +``` + +```rust +// in src/task/mod.rs + +use core::{future::Future, pin::Pin}; +use alloc::boxed::Box; + +pub struct Task { + future: Pin>>, +} +``` + +The `Task` struct is a newtype wrapper around a pinned, heap-allocated, and dynamically dispatched future with the empty type `()` as output. Let's go through it in detail: + +- We require that the future associated with a task returns `()`. This means that tasks don't return any result, they are just executed for their side effects. For example, the `example_task` function we defined above has no return value, but it prints something to the screen as a side effect. +- The `dyn` keyword indicates that we store a [_trait object_] in the `Box`. This means that the methods on the future are [_dynamically dispatched_], allowing different types of futures to be stored in the `Task` type. This is important because each `async fn` has its own type and we want to be able to create multiple different tasks. +- As we learned in the [section about pinning], the `Pin` type ensures that a value cannot be moved in memory by placing it on the heap and preventing the creation of `&mut` references to it. This is important because futures generated by async/await might be self-referential, i.e., contain pointers to themselves that would be invalidated when the future is moved. + +[_trait object_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html +[_dynamically dispatched_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html#trait-objects-perform-dynamic-dispatch +[section about pinning]: #pinning + +To allow the creation of new `Task` structs from futures, we create a `new` function: + +```rust +// in src/task/mod.rs + +impl Task { + pub fn new(future: impl Future + 'static) -> Task { + Task { + future: Box::pin(future), + } + } +} +``` + +The function takes an arbitrary future with an output type of `()` and pins it in memory through the [`Box::pin`] function. Then it wraps the boxed future in the `Task` struct and returns it. The `'static` lifetime is required here because the returned `Task` can live for an arbitrary time, so the future needs to be valid for that time too. + +We also add a `poll` method to allow the executor to poll the stored future: + +```rust +// in src/task/mod.rs + +use core::task::{Context, Poll}; + +impl Task { + fn poll(&mut self, context: &mut Context) -> Poll<()> { + self.future.as_mut().poll(context) + } +} +``` + +Since the [`poll`] method of the `Future` trait expects to be called on a `Pin<&mut T>` type, we use the [`Pin::as_mut`] method to convert the `self.future` field of type `Pin>` first. Then we call `poll` on the converted `self.future` field and return the result. Since the `Task::poll` method should only be called by the executor that we'll create in a moment, we keep the function private to the `task` module. + +### Simple Executor + +Since executors can be quite complex, we deliberately start by creating a very basic executor before implementing a more featureful executor later. For this, we first create a new `task::simple_executor` submodule: + +```rust +// in src/task/mod.rs + +pub mod simple_executor; +``` + +```rust +// in src/task/simple_executor.rs + +use super::Task; +use alloc::collections::VecDeque; + +pub struct SimpleExecutor { + task_queue: VecDeque, +} + +impl SimpleExecutor { + pub fn new() -> SimpleExecutor { + SimpleExecutor { + task_queue: VecDeque::new(), + } + } + + pub fn spawn(&mut self, task: Task) { + self.task_queue.push_back(task) + } +} +``` + +The struct contains a single `task_queue` field of type [`VecDeque`], which is basically a vector that allows for push and pop operations on both ends. The idea behind using this type is that we insert new tasks through the `spawn` method at the end and pop the next task for execution from the front. This way, we get a simple [FIFO queue] (_"first in, first out"_). + +[`VecDeque`]: https://doc.rust-lang.org/stable/alloc/collections/vec_deque/struct.VecDeque.html +[FIFO queue]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics) + +#### Dummy Waker + +In order to call the `poll` method, we need to create a [`Context`] type, which wraps a [`Waker`] type. To start simple, we will first create a dummy waker that does nothing. For this, we create a [`RawWaker`] instance, which defines the implementation of the different `Waker` methods, and then use the [`Waker::from_raw`] function to turn it into a `Waker`: + +[`RawWaker`]: https://doc.rust-lang.org/stable/core/task/struct.RawWaker.html +[`Waker::from_raw`]: https://doc.rust-lang.org/stable/core/task/struct.Waker.html#method.from_raw + +```rust +// in src/task/simple_executor.rs + +use core::task::{Waker, RawWaker}; + +fn dummy_raw_waker() -> RawWaker { + todo!(); +} + +fn dummy_waker() -> Waker { + unsafe { Waker::from_raw(dummy_raw_waker()) } +} +``` + +The `from_raw` function is unsafe because undefined behavior can occur if the programmer does not uphold the documented requirements of `RawWaker`. Before we look at the implementation of the `dummy_raw_waker` function, we first try to understand how the `RawWaker` type works. + +##### `RawWaker` + +The [`RawWaker`] type requires the programmer to explicitly define a [_virtual method table_] (_vtable_) that specifies the functions that should be called when the `RawWaker` is cloned, woken, or dropped. The layout of this vtable is defined by the [`RawWakerVTable`] type. Each function receives a `*const ()` argument, which is a _type-erased_ pointer to some value. The reason for using a `*const ()` pointer instead of a proper reference is that the `RawWaker` type should be non-generic but still support arbitrary types. The pointer is provided by putting it into the `data` argument of [`RawWaker::new`], which just initializes a `RawWaker`. The `Waker` then uses this `RawWaker` to call the vtable functions with `data`. + +[_virtual method table_]: https://en.wikipedia.org/wiki/Virtual_method_table +[`RawWakerVTable`]: https://doc.rust-lang.org/stable/core/task/struct.RawWakerVTable.html +[`RawWaker::new`]: https://doc.rust-lang.org/stable/core/task/struct.RawWaker.html#method.new + +Typically, the `RawWaker` is created for some heap-allocated struct that is wrapped into the [`Box`] or [`Arc`] type. For such types, methods like [`Box::into_raw`] can be used to convert the `Box` to a `*const T` pointer. This pointer can then be cast to an anonymous `*const ()` pointer and passed to `RawWaker::new`. Since each vtable function receives the same `*const ()` as an argument, the functions can safely cast the pointer back to a `Box` or a `&T` to operate on it. As you can imagine, this process is highly dangerous and can easily lead to undefined behavior on mistakes. For this reason, manually creating a `RawWaker` is not recommended unless necessary. + +[`Box`]: https://doc.rust-lang.org/stable/alloc/boxed/struct.Box.html +[`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html +[`Box::into_raw`]: https://doc.rust-lang.org/stable/alloc/boxed/struct.Box.html#method.into_raw + +##### A Dummy `RawWaker` + +While manually creating a `RawWaker` is not recommended, there is currently no other way to create a dummy `Waker` that does nothing. Fortunately, the fact that we want to do nothing makes it relatively safe to implement the `dummy_raw_waker` function: + +```rust +// in src/task/simple_executor.rs + +use core::task::RawWakerVTable; + +fn dummy_raw_waker() -> RawWaker { + fn no_op(_: *const ()) {} + fn clone(_: *const ()) -> RawWaker { + dummy_raw_waker() + } + + let vtable = &RawWakerVTable::new(clone, no_op, no_op, no_op); + RawWaker::new(0 as *const (), vtable) +} +``` + +First, we define two inner functions named `no_op` and `clone`. The `no_op` function takes a `*const ()` pointer and does nothing. The `clone` function also takes a `*const ()` pointer and returns a new `RawWaker` by calling `dummy_raw_waker` again. We use these two functions to create a minimal `RawWakerVTable`: The `clone` function is used for the cloning operations, and the `no_op` function is used for all other operations. Since the `RawWaker` does nothing, it does not matter that we return a new `RawWaker` from `clone` instead of cloning it. + +After creating the `vtable`, we use the [`RawWaker::new`] function to create the `RawWaker`. The passed `*const ()` does not matter since none of the vtable functions use it. For this reason, we simply pass a null pointer. + +#### A `run` Method + +Now we have a way to create a `Waker` instance, we can use it to implement a `run` method on our executor. The most simple `run` method is to repeatedly poll all queued tasks in a loop until all are done. This is not very efficient since it does not utilize the notifications of the `Waker` type, but it is an easy way to get things running: + +```rust +// in src/task/simple_executor.rs + +use core::task::{Context, Poll}; + +impl SimpleExecutor { + pub fn run(&mut self) { + while let Some(mut task) = self.task_queue.pop_front() { + let waker = dummy_waker(); + let mut context = Context::from_waker(&waker); + match task.poll(&mut context) { + Poll::Ready(()) => {} // task done + Poll::Pending => self.task_queue.push_back(task), + } + } + } +} +``` + +The function uses a `while let` loop to handle all tasks in the `task_queue`. For each task, it first creates a `Context` type by wrapping a `Waker` instance returned by our `dummy_waker` function. Then it invokes the `Task::poll` method with this `context`. If the `poll` method returns `Poll::Ready`, the task is finished and we can continue with the next task. If the task is still `Poll::Pending`, we add it to the back of the queue again so that it will be polled again in a subsequent loop iteration. + +#### Trying It + +With our `SimpleExecutor` type, we can now try running the task returned by the `example_task` function in our `main.rs`: + +```rust +// in src/main.rs + +use blog_os::task::{Task, simple_executor::SimpleExecutor}; + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + // […] initialization routines, including `init_heap` + + let mut executor = SimpleExecutor::new(); + executor.spawn(Task::new(example_task())); + executor.run(); + + // […] test_main, "it did not crash" message, hlt_loop +} + + +// Below is the example_task function again so that you don't have to scroll up + +async fn async_number() -> u32 { + 42 +} + +async fn example_task() { + let number = async_number().await; + println!("async number: {}", number); +} +``` + +When we run it, we see that the expected _"async number: 42"_ message is printed to the screen: + +![QEMU printing "Hello World", "async number: 42", and "It did not crash!"](qemu-simple-executor.png) + +Let's summarize the various steps that happen in this example: + +- First, a new instance of our `SimpleExecutor` type is created with an empty `task_queue`. +- Next, we call the asynchronous `example_task` function, which returns a future. We wrap this future in the `Task` type, which moves it to the heap and pins it, and then add the task to the `task_queue` of the executor through the `spawn` method. +- We then call the `run` method to start the execution of the single task in the queue. This involves: + - Popping the task from the front of the `task_queue`. + - Creating a `RawWaker` for the task, converting it to a [`Waker`] instance, and then creating a [`Context`] instance from it. + - Calling the [`poll`] method on the future of the task, using the `Context` we just created. + - Since the `example_task` does not wait for anything, it can directly run till its end on the first `poll` call. This is where the _"async number: 42"_ line is printed. + - Since the `example_task` directly returns `Poll::Ready`, it is not added back to the task queue. +- The `run` method returns after the `task_queue` becomes empty. The execution of our `kernel_main` function continues and the _"It did not crash!"_ message is printed. + +### Async Keyboard Input + +Our simple executor does not utilize the `Waker` notifications and simply loops over all tasks until they are done. This wasn't a problem for our example since our `example_task` can directly run to finish on the first `poll` call. To see the performance advantages of a proper `Waker` implementation, we first need to create a task that is truly asynchronous, i.e., a task that will probably return `Poll::Pending` on the first `poll` call. + +We already have some kind of asynchronicity in our system that we can use for this: hardware interrupts. As we learned in the [_Interrupts_] post, hardware interrupts can occur at arbitrary points in time, determined by some external device. For example, a hardware timer sends an interrupt to the CPU after some predefined time has elapsed. When the CPU receives an interrupt, it immediately transfers control to the corresponding handler function defined in the interrupt descriptor table (IDT). + +[_Interrupts_]: @/edition-2/posts/07-hardware-interrupts/index.md + +In the following, we will create an asynchronous task based on the keyboard interrupt. The keyboard interrupt is a good candidate for this because it is both non-deterministic and latency-critical. Non-deterministic means that there is no way to predict when the next key press will occur because it is entirely dependent on the user. Latency-critical means that we want to handle the keyboard input in a timely manner, otherwise the user will feel a lag. To support such a task in an efficient way, it will be essential that the executor has proper support for `Waker` notifications. + +#### Scancode Queue + +Currently, we handle the keyboard input directly in the interrupt handler. This is not a good idea for the long term because interrupt handlers should stay as short as possible as they might interrupt important work. Instead, interrupt handlers should only perform the minimal amount of work necessary (e.g., reading the keyboard scancode) and leave the rest of the work (e.g., interpreting the scancode) to a background task. + +A common pattern for delegating work to a background task is to create some sort of queue. The interrupt handler pushes units of work to the queue, and the background task handles the work in the queue. Applied to our keyboard interrupt, this means that the interrupt handler only reads the scancode from the keyboard, pushes it to the queue, and then returns. The keyboard task sits on the other end of the queue and interprets and handles each scancode that is pushed to it: + +![Scancode queue with 8 slots on the top. Keyboard interrupt handler on the bottom left with a "push scancode" arrow to the left of the queue. Keyboard task on the bottom right with a "pop scancode" arrow coming from the right side of the queue.](scancode-queue.svg) + +A simple implementation of that queue could be a mutex-protected [`VecDeque`]. However, using mutexes in interrupt handlers is not a good idea since it can easily lead to deadlocks. For example, when the user presses a key while the keyboard task has locked the queue, the interrupt handler tries to acquire the lock again and hangs indefinitely. Another problem with this approach is that `VecDeque` automatically increases its capacity by performing a new heap allocation when it becomes full. This can lead to deadlocks again because our allocator also uses a mutex internally. Further problems are that heap allocations can fail or take a considerable amount of time when the heap is fragmented. + +To prevent these problems, we need a queue implementation that does not require mutexes or allocations for its `push` operation. Such queues can be implemented by using lock-free [atomic operations] for pushing and popping elements. This way, it is possible to create `push` and `pop` operations that only require a `&self` reference and are thus usable without a mutex. To avoid allocations on `push`, the queue can be backed by a pre-allocated fixed-size buffer. While this makes the queue _bounded_ (i.e., it has a maximum length), it is often possible to define reasonable upper bounds for the queue length in practice, so that this isn't a big problem. + +[atomic operations]: https://doc.rust-lang.org/core/sync/atomic/index.html + +##### The `crossbeam` Crate + +Implementing such a queue in a correct and efficient way is very difficult, so I recommend sticking to existing, well-tested implementations. One popular Rust project that implements various mutex-free types for concurrent programming is [`crossbeam`]. It provides a type named [`ArrayQueue`] that is exactly what we need in this case. And we're lucky: the type is fully compatible with `no_std` crates with allocation support. + +[`crossbeam`]: https://github.com/crossbeam-rs/crossbeam +[`ArrayQueue`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html + +To use the type, we need to add a dependency on the `crossbeam-queue` crate: + +```toml +# in Cargo.toml + +[dependencies.crossbeam-queue] +version = "0.3.11" +default-features = false +features = ["alloc"] +``` + +By default, the crate depends on the standard library. To make it `no_std` compatible, we need to disable its default features and instead enable the `alloc` feature. (Note that we could also add a dependency on the main `crossbeam` crate, which re-exports the `crossbeam-queue` crate, but this would result in a larger number of dependencies and longer compile times.) + +##### Queue Implementation + +Using the `ArrayQueue` type, we can now create a global scancode queue in a new `task::keyboard` module: + +```rust +// in src/task/mod.rs + +pub mod keyboard; +``` + +```rust +// in src/task/keyboard.rs + +use conquer_once::spin::OnceCell; +use crossbeam_queue::ArrayQueue; + +static SCANCODE_QUEUE: OnceCell> = OnceCell::uninit(); +``` + +Since [`ArrayQueue::new`] performs a heap allocation, which is not possible at compile time ([yet][const-heap-alloc]), we can't initialize the static variable directly. Instead, we use the [`OnceCell`] type of the [`conquer_once`] crate, which makes it possible to perform a safe one-time initialization of static values. To include the crate, we need to add it as a dependency in our `Cargo.toml`: + +[`ArrayQueue::new`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.new +[const-heap-alloc]: https://github.com/rust-lang/const-eval/issues/20 +[`OnceCell`]: https://docs.rs/conquer-once/0.2.0/conquer_once/raw/struct.OnceCell.html +[`conquer_once`]: https://docs.rs/conquer-once/0.2.0/conquer_once/index.html + +```toml +# in Cargo.toml + +[dependencies.conquer-once] +version = "0.2.0" +default-features = false +``` + +Instead of the [`OnceCell`] primitive, we could also use the [`lazy_static`] macro here. However, the `OnceCell` type has the advantage that we can ensure that the initialization does not happen in the interrupt handler, thus preventing the interrupt handler from performing a heap allocation. + +[`lazy_static`]: https://docs.rs/lazy_static/1.4.0/lazy_static/index.html + +#### Filling the Queue + +To fill the scancode queue, we create a new `add_scancode` function that we will call from the interrupt handler: + +```rust +// in src/task/keyboard.rs + +use crate::println; + +/// Called by the keyboard interrupt handler +/// +/// Must not block or allocate. +pub(crate) fn add_scancode(scancode: u8) { + if let Ok(queue) = SCANCODE_QUEUE.try_get() { + if let Err(_) = queue.push(scancode) { + println!("WARNING: scancode queue full; dropping keyboard input"); + } + } else { + println!("WARNING: scancode queue uninitialized"); + } +} +``` + +We use [`OnceCell::try_get`] to get a reference to the initialized queue. If the queue is not initialized yet, we ignore the keyboard scancode and print a warning. It's important that we don't try to initialize the queue in this function because it will be called by the interrupt handler, which should not perform heap allocations. Since this function should not be callable from our `main.rs`, we use the `pub(crate)` visibility to make it only available to our `lib.rs`. + +[`OnceCell::try_get`]: https://docs.rs/conquer-once/0.2.0/conquer_once/raw/struct.OnceCell.html#method.try_get + +The fact that the [`ArrayQueue::push`] method requires only a `&self` reference makes it very simple to call the method on the static queue. The `ArrayQueue` type performs all the necessary synchronization itself, so we don't need a mutex wrapper here. In case the queue is full, we print a warning too. + +[`ArrayQueue::push`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.push + +To call the `add_scancode` function on keyboard interrupts, we update our `keyboard_interrupt_handler` function in the `interrupts` module: + +```rust +// in src/interrupts.rs + +extern "x86-interrupt" fn keyboard_interrupt_handler( + _stack_frame: InterruptStackFrame +) { + use x86_64::instructions::port::Port; + + let mut port = Port::new(0x60); + let scancode: u8 = unsafe { port.read() }; + crate::task::keyboard::add_scancode(scancode); // new + + unsafe { + PICS.lock() + .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8()); + } +} +``` + +We removed all the keyboard handling code from this function and instead added a call to the `add_scancode` function. The rest of the function stays the same as before. + +As expected, keypresses are no longer printed to the screen when we run our project using `cargo run` now. Instead, we see the warning that the scancode queue is uninitialized for every keystroke. + +#### Scancode Stream + +To initialize the `SCANCODE_QUEUE` and read the scancodes from the queue in an asynchronous way, we create a new `ScancodeStream` type: + +```rust +// in src/task/keyboard.rs + +pub struct ScancodeStream { + _private: (), +} + +impl ScancodeStream { + pub fn new() -> Self { + SCANCODE_QUEUE.try_init_once(|| ArrayQueue::new(100)) + .expect("ScancodeStream::new should only be called once"); + ScancodeStream { _private: () } + } +} +``` + +The purpose of the `_private` field is to prevent construction of the struct from outside of the module. This makes the `new` function the only way to construct the type. In the function, we first try to initialize the `SCANCODE_QUEUE` static. We panic if it is already initialized to ensure that only a single `ScancodeStream` instance can be created. + +To make the scancodes available to asynchronous tasks, the next step is to implement a `poll`-like method that tries to pop the next scancode off the queue. While this sounds like we should implement the [`Future`] trait for our type, this does not quite fit here. The problem is that the `Future` trait only abstracts over a single asynchronous value and expects that the `poll` method is not called again after it returns `Poll::Ready`. Our scancode queue, however, contains multiple asynchronous values, so it is okay to keep polling it. + +##### The `Stream` Trait + +Since types that yield multiple asynchronous values are common, the [`futures`] crate provides a useful abstraction for such types: the [`Stream`] trait. The trait is defined like this: + +[`Stream`]: https://rust-lang.github.io/async-book/05_streams/01_chapter.html + +```rust +pub trait Stream { + type Item; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) + -> Poll>; +} +``` + +This definition is quite similar to the [`Future`] trait, with the following differences: + +- The associated type is named `Item` instead of `Output`. +- Instead of a `poll` method that returns `Poll`, the `Stream` trait defines a `poll_next` method that returns a `Poll>` (note the additional `Option`). + +There is also a semantic difference: The `poll_next` can be called repeatedly, until it returns `Poll::Ready(None)` to signal that the stream is finished. In this regard, the method is similar to the [`Iterator::next`] method, which also returns `None` after the last value. + +[`Iterator::next`]: https://doc.rust-lang.org/stable/core/iter/trait.Iterator.html#tymethod.next + +##### Implementing `Stream` + +Let's implement the `Stream` trait for our `ScancodeStream` to provide the values of the `SCANCODE_QUEUE` in an asynchronous way. For this, we first need to add a dependency on the `futures-util` crate, which contains the `Stream` type: + +```toml +# in Cargo.toml + +[dependencies.futures-util] +version = "0.3.4" +default-features = false +features = ["alloc"] +``` + +We disable the default features to make the crate `no_std` compatible and enable the `alloc` feature to make its allocation-based types available (we will need this later). (Note that we could also add a dependency on the main `futures` crate, which re-exports the `futures-util` crate, but this would result in a larger number of dependencies and longer compile times.) + +Now we can import and implement the `Stream` trait: + +```rust +// in src/task/keyboard.rs + +use core::{pin::Pin, task::{Poll, Context}}; +use futures_util::stream::Stream; + +impl Stream for ScancodeStream { + type Item = u8; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let queue = SCANCODE_QUEUE.try_get().expect("not initialized"); + match queue.pop() { + Some(scancode) => Poll::Ready(Some(scancode)), + None => Poll::Pending, + } + } +} +``` + +We first use the [`OnceCell::try_get`] method to get a reference to the initialized scancode queue. This should never fail since we initialize the queue in the `new` function, so we can safely use the `expect` method to panic if it's not initialized. Next, we use the [`ArrayQueue::pop`] method to try to get the next element from the queue. If it succeeds, we return the scancode wrapped in `Poll::Ready(Some(…))`. If it fails, it means that the queue is empty. In that case, we return `Poll::Pending`. + +[`ArrayQueue::pop`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.pop + +#### Waker Support + +Like the `Futures::poll` method, the `Stream::poll_next` method requires the asynchronous task to notify the executor when it becomes ready after `Poll::Pending` is returned. This way, the executor does not need to poll the same task again until it is notified, which greatly reduces the performance overhead of waiting tasks. + +To send this notification, the task should extract the [`Waker`] from the passed [`Context`] reference and store it somewhere. When the task becomes ready, it should invoke the [`wake`] method on the stored `Waker` to notify the executor that the task should be polled again. + +##### AtomicWaker + +To implement the `Waker` notification for our `ScancodeStream`, we need a place where we can store the `Waker` between poll calls. We can't store it as a field in the `ScancodeStream` itself because it needs to be accessible from the `add_scancode` function. The solution to this is to use a static variable of the [`AtomicWaker`] type provided by the `futures-util` crate. Like the `ArrayQueue` type, this type is based on atomic instructions and can be safely stored in a `static` and modified concurrently. + +[`AtomicWaker`]: https://docs.rs/futures-util/0.3.4/futures_util/task/struct.AtomicWaker.html + +Let's use the [`AtomicWaker`] type to define a static `WAKER`: + +```rust +// in src/task/keyboard.rs + +use futures_util::task::AtomicWaker; + +static WAKER: AtomicWaker = AtomicWaker::new(); +``` + +The idea is that the `poll_next` implementation stores the current waker in this static, and the `add_scancode` function calls the `wake` function on it when a new scancode is added to the queue. + +##### Storing a Waker + +The contract defined by `poll`/`poll_next` requires the task to register a wakeup for the passed `Waker` when it returns `Poll::Pending`. Let's modify our `poll_next` implementation to satisfy this requirement: + +```rust +// in src/task/keyboard.rs + +impl Stream for ScancodeStream { + type Item = u8; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let queue = SCANCODE_QUEUE + .try_get() + .expect("scancode queue not initialized"); + + // fast path + if let Some(scancode) = queue.pop() { + return Poll::Ready(Some(scancode)); + } + + WAKER.register(&cx.waker()); + match queue.pop() { + Some(scancode) => { + WAKER.take(); + Poll::Ready(Some(scancode)) + } + None => Poll::Pending, + } + } +} +``` + +Like before, we first use the [`OnceCell::try_get`] function to get a reference to the initialized scancode queue. We then optimistically try to `pop` from the queue and return `Poll::Ready` when it succeeds. This way, we can avoid the performance overhead of registering a waker when the queue is not empty. + +If the first call to `queue.pop()` does not succeed, the queue is potentially empty. Only potentially because the interrupt handler might have filled the queue asynchronously immediately after the check. Since this race condition can occur again for the next check, we need to register the `Waker` in the `WAKER` static before the second check. This way, a wakeup might happen before we return `Poll::Pending`, but it is guaranteed that we get a wakeup for any scancodes pushed after the check. + +After registering the `Waker` contained in the passed [`Context`] through the [`AtomicWaker::register`] function, we try to pop from the queue a second time. If it now succeeds, we return `Poll::Ready`. We also remove the registered waker again using [`AtomicWaker::take`] because a waker notification is no longer needed. In case `queue.pop()` fails for a second time, we return `Poll::Pending` like before, but this time with a registered wakeup. + +[`AtomicWaker::register`]: https://docs.rs/futures-util/0.3.4/futures_util/task/struct.AtomicWaker.html#method.register +[`AtomicWaker::take`]: https://docs.rs/futures/0.3.4/futures/task/struct.AtomicWaker.html#method.take + +Note that there are two ways that a wakeup can happen for a task that did not return `Poll::Pending` (yet). One way is the mentioned race condition when the wakeup happens immediately before returning `Poll::Pending`. The other way is when the queue is no longer empty after registering the waker, so that `Poll::Ready` is returned. Since these spurious wakeups are not preventable, the executor needs to be able to handle them correctly. + +##### Waking the Stored Waker + +To wake the stored `Waker`, we add a call to `WAKER.wake()` in the `add_scancode` function: + +```rust +// in src/task/keyboard.rs + +pub(crate) fn add_scancode(scancode: u8) { + if let Ok(queue) = SCANCODE_QUEUE.try_get() { + if let Err(_) = queue.push(scancode) { + println!("WARNING: scancode queue full; dropping keyboard input"); + } else { + WAKER.wake(); // new + } + } else { + println!("WARNING: scancode queue uninitialized"); + } +} +``` + +The only change that we made is to add a call to `WAKER.wake()` if the push to the scancode queue succeeds. If a waker is registered in the `WAKER` static, this method will call the equally-named [`wake`] method on it, which notifies the executor. Otherwise, the operation is a no-op, i.e., nothing happens. + +[`wake`]: https://doc.rust-lang.org/stable/core/task/struct.Waker.html#method.wake + +It is important that we call `wake` only after pushing to the queue because otherwise the task might be woken too early while the queue is still empty. This can, for example, happen when using a multi-threaded executor that starts the woken task concurrently on a different CPU core. While we don't have thread support yet, we will add it soon and don't want things to break then. + +#### Keyboard Task + +Now that we implemented the `Stream` trait for our `ScancodeStream`, we can use it to create an asynchronous keyboard task: + +```rust +// in src/task/keyboard.rs + +use futures_util::stream::StreamExt; +use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1}; +use crate::print; + +pub async fn print_keypresses() { + let mut scancodes = ScancodeStream::new(); + let mut keyboard = Keyboard::new(ScancodeSet1::new(), + layouts::Us104Key, HandleControl::Ignore); + + while let Some(scancode) = scancodes.next().await { + if let Ok(Some(key_event)) = keyboard.add_byte(scancode) { + if let Some(key) = keyboard.process_keyevent(key_event) { + match key { + DecodedKey::Unicode(character) => print!("{}", character), + DecodedKey::RawKey(key) => print!("{:?}", key), + } + } + } + } +} +``` + +The code is very similar to the code we had in our [keyboard interrupt handler] before we modified it in this post. The only difference is that, instead of reading the scancode from an I/O port, we take it from the `ScancodeStream`. For this, we first create a new `Scancode` stream and then repeatedly use the [`next`] method provided by the [`StreamExt`] trait to get a `Future` that resolves to the next element in the stream. By using the `await` operator on it, we asynchronously wait for the result of the future. + +[keyboard interrupt handler]: @/edition-2/posts/07-hardware-interrupts/index.md#interpreting-the-scancodes +[`next`]: https://docs.rs/futures-util/0.3.4/futures_util/stream/trait.StreamExt.html#method.next +[`StreamExt`]: https://docs.rs/futures-util/0.3.4/futures_util/stream/trait.StreamExt.html + +We use `while let` to loop until the stream returns `None` to signal its end. Since our `poll_next` method never returns `None`, this is effectively an endless loop, so the `print_keypresses` task never finishes. + +Let's add the `print_keypresses` task to our executor in our `main.rs` to get working keyboard input again: + +```rust +// in src/main.rs + +use blog_os::task::keyboard; // new + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + + // […] initialization routines, including init_heap, test_main + + let mut executor = SimpleExecutor::new(); + executor.spawn(Task::new(example_task())); + executor.spawn(Task::new(keyboard::print_keypresses())); // new + executor.run(); + + // […] "it did not crash" message, hlt_loop +} +``` + +When we execute `cargo run` now, we see that keyboard input works again: + +![QEMU printing ".....H...e...l...l..o..... ...W..o..r....l...d...!"](qemu-keyboard-output.gif) + +If you keep an eye on the CPU utilization of your computer, you will see that the `QEMU` process now continuously keeps the CPU busy. This happens because our `SimpleExecutor` polls tasks over and over again in a loop. So even if we don't press any keys on the keyboard, the executor repeatedly calls `poll` on our `print_keypresses` task, even though the task cannot make any progress and will return `Poll::Pending` each time. + +### Executor with Waker Support + +To fix the performance problem, we need to create an executor that properly utilizes the `Waker` notifications. This way, the executor is notified when the next keyboard interrupt occurs, so it does not need to keep polling the `print_keypresses` task over and over again. + +#### Task Id + +The first step in creating an executor with proper support for waker notifications is to give each task a unique ID. This is required because we need a way to specify which task should be woken. We start by creating a new `TaskId` wrapper type: + +```rust +// in src/task/mod.rs + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct TaskId(u64); +``` + +The `TaskId` struct is a simple wrapper type around `u64`. We derive a number of traits for it to make it printable, copyable, comparable, and sortable. The latter is important because we want to use `TaskId` as the key type of a [`BTreeMap`] in a moment. + +[`BTreeMap`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html + +To create a new unique ID, we create a `TaskId::new` function: + +```rust +use core::sync::atomic::{AtomicU64, Ordering}; + +impl TaskId { + fn new() -> Self { + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + TaskId(NEXT_ID.fetch_add(1, Ordering::Relaxed)) + } +} +``` + +The function uses a static `NEXT_ID` variable of type [`AtomicU64`] to ensure that each ID is assigned only once. The [`fetch_add`] method atomically increases the value and returns the previous value in one atomic operation. This means that even when the `TaskId::new` method is called in parallel, every ID is returned exactly once. The [`Ordering`] parameter defines whether the compiler is allowed to reorder the `fetch_add` operation in the instructions stream. Since we only require that the ID be unique, the `Relaxed` ordering with the weakest requirements is enough in this case. + +[`AtomicU64`]: https://doc.rust-lang.org/core/sync/atomic/struct.AtomicU64.html +[`fetch_add`]: https://doc.rust-lang.org/core/sync/atomic/struct.AtomicU64.html#method.fetch_add +[`Ordering`]: https://doc.rust-lang.org/core/sync/atomic/enum.Ordering.html + +We can now extend our `Task` type with an additional `id` field: + +```rust +// in src/task/mod.rs + +pub struct Task { + id: TaskId, // new + future: Pin>>, +} + +impl Task { + pub fn new(future: impl Future + 'static) -> Task { + Task { + id: TaskId::new(), // new + future: Box::pin(future), + } + } +} +``` + +The new `id` field makes it possible to uniquely name a task, which is required for waking a specific task. + +#### The `Executor` Type + +We create our new `Executor` type in a `task::executor` module: + +```rust +// in src/task/mod.rs + +pub mod executor; +``` + +```rust +// in src/task/executor.rs + +use super::{Task, TaskId}; +use alloc::{collections::BTreeMap, sync::Arc}; +use core::task::Waker; +use crossbeam_queue::ArrayQueue; + +pub struct Executor { + tasks: BTreeMap, + task_queue: Arc>, + waker_cache: BTreeMap, +} + +impl Executor { + pub fn new() -> Self { + Executor { + tasks: BTreeMap::new(), + task_queue: Arc::new(ArrayQueue::new(100)), + waker_cache: BTreeMap::new(), + } + } +} +``` + +Instead of storing tasks in a [`VecDeque`] like we did for our `SimpleExecutor`, we use a `task_queue` of task IDs and a [`BTreeMap`] named `tasks` that contains the actual `Task` instances. The map is indexed by the `TaskId` to allow efficient continuation of a specific task. + +The `task_queue` field is an [`ArrayQueue`] of task IDs, wrapped into the [`Arc`] type that implements _reference counting_. Reference counting makes it possible to share ownership of the value among multiple owners. It works by allocating the value on the heap and counting the number of active references to it. When the number of active references reaches zero, the value is no longer needed and can be deallocated. + +We use this `Arc` type for the `task_queue` because it will be shared between the executor and wakers. The idea is that the wakers push the ID of the woken task to the queue. The executor sits on the receiving end of the queue, retrieves the woken tasks by their ID from the `tasks` map, and then runs them. The reason for using a fixed-size queue instead of an unbounded queue such as [`SegQueue`] is that interrupt handlers should not allocate on push to this queue. + +In addition to the `task_queue` and the `tasks` map, the `Executor` type has a `waker_cache` field that is also a map. This map caches the [`Waker`] of a task after its creation. This has two reasons: First, it improves performance by reusing the same waker for multiple wake-ups of the same task instead of creating a new waker each time. Second, it ensures that reference-counted wakers are not deallocated inside interrupt handlers because it could lead to deadlocks (there are more details on this below). + +[`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html +[`SegQueue`]: https://docs.rs/crossbeam-queue/0.2.1/crossbeam_queue/struct.SegQueue.html + +To create an `Executor`, we provide a simple `new` function. We choose a capacity of 100 for the `task_queue`, which should be more than enough for the foreseeable future. In case our system will have more than 100 concurrent tasks at some point, we can easily increase this size. + +#### Spawning Tasks + +As for the `SimpleExecutor`, we provide a `spawn` method on our `Executor` type that adds a given task to the `tasks` map and immediately wakes it by pushing its ID to the `task_queue`: + +```rust +// in src/task/executor.rs + +impl Executor { + pub fn spawn(&mut self, task: Task) { + let task_id = task.id; + if self.tasks.insert(task.id, task).is_some() { + panic!("task with same ID already in tasks"); + } + self.task_queue.push(task_id).expect("queue full"); + } +} +``` + +If there is already a task with the same ID in the map, the [`BTreeMap::insert`] method returns it. This should never happen since each task has a unique ID, so we panic in this case since it indicates a bug in our code. Similarly, we panic when the `task_queue` is full since this should never happen if we choose a large-enough queue size. + +#### Running Tasks + +To execute all tasks in the `task_queue`, we create a private `run_ready_tasks` method: + +```rust +// in src/task/executor.rs + +use core::task::{Context, Poll}; + +impl Executor { + fn run_ready_tasks(&mut self) { + // destructure `self` to avoid borrow checker errors + let Self { + tasks, + task_queue, + waker_cache, + } = self; + + while let Some(task_id) = task_queue.pop() { + let task = match tasks.get_mut(&task_id) { + Some(task) => task, + None => continue, // task no longer exists + }; + let waker = waker_cache + .entry(task_id) + .or_insert_with(|| TaskWaker::new(task_id, task_queue.clone())); + let mut context = Context::from_waker(waker); + match task.poll(&mut context) { + Poll::Ready(()) => { + // task done -> remove it and its cached waker + tasks.remove(&task_id); + waker_cache.remove(&task_id); + } + Poll::Pending => {} + } + } + } +} +``` + +The basic idea of this function is similar to our `SimpleExecutor`: Loop over all tasks in the `task_queue`, create a waker for each task, and then poll them. However, instead of adding pending tasks back to the end of the `task_queue`, we let our `TaskWaker` implementation take care of adding woken tasks back to the queue. The implementation of this waker type will be shown in a moment. + +Let's look into some of the implementation details of this `run_ready_tasks` method: + +- We use [_destructuring_] to split `self` into its three fields to avoid some borrow checker errors. Namely, our implementation needs to access the `self.task_queue` from within a closure, which currently tries to borrow `self` completely. This is a fundamental borrow checker issue that will be resolved when [RFC 2229] is [implemented][RFC 2229 impl]. + +- For each popped task ID, we retrieve a mutable reference to the corresponding task from the `tasks` map. Since our `ScancodeStream` implementation registers wakers before checking whether a task needs to be put to sleep, it might happen that a wake-up occurs for a task that no longer exists. In this case, we simply ignore the wake-up and continue with the next ID from the queue. + +- To avoid the performance overhead of creating a waker on each poll, we use the `waker_cache` map to store the waker for each task after it has been created. For this, we use the [`BTreeMap::entry`] method in combination with [`Entry::or_insert_with`] to create a new waker if it doesn't exist yet and then get a mutable reference to it. For creating a new waker, we clone the `task_queue` and pass it together with the task ID to the `TaskWaker::new` function (implementation shown below). Since the `task_queue` is wrapped into an `Arc`, the `clone` only increases the reference count of the value, but still points to the same heap-allocated queue. Note that reusing wakers like this is not possible for all waker implementations, but our `TaskWaker` type will allow it. + +[_destructuring_]: https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#destructuring-to-break-apart-values +[RFC 2229]: https://github.com/rust-lang/rfcs/pull/2229 +[RFC 2229 impl]: https://github.com/rust-lang/rust/issues/53488 + +[`BTreeMap::entry`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html#method.entry +[`Entry::or_insert_with`]: https://doc.rust-lang.org/alloc/collections/btree_map/enum.Entry.html#method.or_insert_with + +A task is finished when it returns `Poll::Ready`. In that case, we remove it from the `tasks` map using the [`BTreeMap::remove`] method. We also remove its cached waker, if it exists. + +[`BTreeMap::remove`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html#method.remove + +#### Waker Design + +The job of the waker is to push the ID of the woken task to the `task_queue` of the executor. We implement this by creating a new `TaskWaker` struct that stores the task ID and a reference to the `task_queue`: + +```rust +// in src/task/executor.rs + +struct TaskWaker { + task_id: TaskId, + task_queue: Arc>, +} +``` + +Since the ownership of the `task_queue` is shared between the executor and wakers, we use the [`Arc`] wrapper type to implement shared reference-counted ownership. + +[`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html + +The implementation of the wake operation is quite simple: + +```rust +// in src/task/executor.rs + +impl TaskWaker { + fn wake_task(&self) { + self.task_queue.push(self.task_id).expect("task_queue full"); + } +} +``` + +We push the `task_id` to the referenced `task_queue`. Since modifications to the [`ArrayQueue`] type only require a shared reference, we can implement this method on `&self` instead of `&mut self`. + +##### The `Wake` Trait + +In order to use our `TaskWaker` type for polling futures, we need to convert it to a [`Waker`] instance first. This is required because the [`Future::poll`] method takes a [`Context`] instance as an argument, which can only be constructed from the `Waker` type. While we could do this by providing an implementation of the [`RawWaker`] type, it's both simpler and safer to instead implement the `Arc`-based [`Wake`][wake-trait] trait and then use the [`From`] implementations provided by the standard library to construct the `Waker`. + +The trait implementation looks like this: + +[wake-trait]: https://doc.rust-lang.org/nightly/alloc/task/trait.Wake.html + +```rust +// in src/task/executor.rs + +use alloc::task::Wake; + +impl Wake for TaskWaker { + fn wake(self: Arc) { + self.wake_task(); + } + + fn wake_by_ref(self: &Arc) { + self.wake_task(); + } +} +``` + +Since wakers are commonly shared between the executor and the asynchronous tasks, the trait methods require that the `Self` instance is wrapped in the [`Arc`] type, which implements reference-counted ownership. This means that we have to move our `TaskWaker` to an `Arc` in order to call them. + +The difference between the `wake` and `wake_by_ref` methods is that the latter only requires a reference to the `Arc`, while the former takes ownership of the `Arc` and thus often requires an increase of the reference count. Not all types support waking by reference, so implementing the `wake_by_ref` method is optional. However, it can lead to better performance because it avoids unnecessary reference count modifications. In our case, we can simply forward both trait methods to our `wake_task` function, which requires only a shared `&self` reference. + +##### Creating Wakers + +Since the `Waker` type supports [`From`] conversions for all `Arc`-wrapped values that implement the `Wake` trait, we can now implement the `TaskWaker::new` function that is required by our `Executor::run_ready_tasks` method: + +[`From`]: https://doc.rust-lang.org/nightly/core/convert/trait.From.html + +```rust +// in src/task/executor.rs + +impl TaskWaker { + fn new(task_id: TaskId, task_queue: Arc>) -> Waker { + Waker::from(Arc::new(TaskWaker { + task_id, + task_queue, + })) + } +} +``` + +We create the `TaskWaker` using the passed `task_id` and `task_queue`. We then wrap the `TaskWaker` in an `Arc` and use the `Waker::from` implementation to convert it to a [`Waker`]. This `from` method takes care of constructing a [`RawWakerVTable`] and a [`RawWaker`] instance for our `TaskWaker` type. In case you're interested in how it works in detail, check out the [implementation in the `alloc` crate][waker-from-impl]. + +[waker-from-impl]: https://github.com/rust-lang/rust/blob/cdb50c6f2507319f29104a25765bfb79ad53395c/src/liballoc/task.rs#L58-L87 + +#### A `run` Method + +With our waker implementation in place, we can finally construct a `run` method for our executor: + +```rust +// in src/task/executor.rs + +impl Executor { + pub fn run(&mut self) -> ! { + loop { + self.run_ready_tasks(); + } + } +} +``` + +This method just calls the `run_ready_tasks` function in a loop. While we could theoretically return from the function when the `tasks` map becomes empty, this would never happen since our `keyboard_task` never finishes, so a simple `loop` should suffice. Since the function never returns, we use the `!` return type to mark the function as [diverging] to the compiler. + +[diverging]: https://doc.rust-lang.org/stable/rust-by-example/fn/diverging.html + +We can now change our `kernel_main` to use our new `Executor` instead of the `SimpleExecutor`: + +```rust +// in src/main.rs + +use blog_os::task::executor::Executor; // new + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + // […] initialization routines, including init_heap, test_main + + let mut executor = Executor::new(); // new + executor.spawn(Task::new(example_task())); + executor.spawn(Task::new(keyboard::print_keypresses())); + executor.run(); +} +``` + +We only need to change the import and the type name. Since our `run` function is marked as diverging, the compiler knows that it never returns, so we no longer need a call to `hlt_loop` at the end of our `kernel_main` function. + +When we run our kernel using `cargo run` now, we see that keyboard input still works: + +![QEMU printing ".....H...e...l...l..o..... ...a..g..a....i...n...!"](qemu-keyboard-output-again.gif) + +However, the CPU utilization of QEMU did not get any better. The reason for this is that we still keep the CPU busy the whole time. We no longer poll tasks until they are woken again, but we still check the `task_queue` in a busy loop. To fix this, we need to put the CPU to sleep if there is no more work to do. + +#### Sleep If Idle + +The basic idea is to execute the [`hlt` instruction] when the `task_queue` is empty. This instruction puts the CPU to sleep until the next interrupt arrives. The fact that the CPU immediately becomes active again on interrupts ensures that we can still directly react when an interrupt handler pushes to the `task_queue`. + +[`hlt` instruction]: https://en.wikipedia.org/wiki/HLT_(x86_instruction) + +To implement this, we create a new `sleep_if_idle` method in our executor and call it from our `run` method: + +```rust +// in src/task/executor.rs + +impl Executor { + pub fn run(&mut self) -> ! { + loop { + self.run_ready_tasks(); + self.sleep_if_idle(); // new + } + } + + fn sleep_if_idle(&self) { + if self.task_queue.is_empty() { + x86_64::instructions::hlt(); + } + } +} +``` + +Since we call `sleep_if_idle` directly after `run_ready_tasks`, which loops until the `task_queue` becomes empty, checking the queue again might seem unnecessary. However, a hardware interrupt might occur directly after `run_ready_tasks` returns, so there might be a new task in the queue at the time the `sleep_if_idle` function is called. Only if the queue is still empty, do we put the CPU to sleep by executing the `hlt` instruction through the [`instructions::hlt`] wrapper function provided by the [`x86_64`] crate. + +[`instructions::hlt`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/fn.hlt.html +[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/index.html + +Unfortunately, there is still a subtle race condition in this implementation. Since interrupts are asynchronous and can happen at any time, it is possible that an interrupt happens right between the `is_empty` check and the call to `hlt`: + +```rust +if self.task_queue.is_empty() { + /// <--- interrupt can happen here + x86_64::instructions::hlt(); +} +``` + +In case this interrupt pushes to the `task_queue`, we put the CPU to sleep even though there is now a ready task. In the worst case, this could delay the handling of a keyboard interrupt until the next keypress or the next timer interrupt. So how do we prevent it? + +The answer is to disable interrupts on the CPU before the check and atomically enable them again together with the `hlt` instruction. This way, all interrupts that happen in between are delayed after the `hlt` instruction so that no wake-ups are missed. To implement this approach, we can use the [`interrupts::enable_and_hlt`][`enable_and_hlt`] function provided by the [`x86_64`] crate. + +[`enable_and_hlt`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/interrupts/fn.enable_and_hlt.html + +The updated implementation of our `sleep_if_idle` function looks like this: + +```rust +// in src/task/executor.rs + +impl Executor { + fn sleep_if_idle(&self) { + use x86_64::instructions::interrupts::{self, enable_and_hlt}; + + interrupts::disable(); + if self.task_queue.is_empty() { + enable_and_hlt(); + } else { + interrupts::enable(); + } + } +} +``` + +To avoid race conditions, we disable interrupts before checking whether the `task_queue` is empty. If it is, we use the [`enable_and_hlt`] function to enable interrupts and put the CPU to sleep as a single atomic operation. In case the queue is no longer empty, it means that an interrupt woke a task after `run_ready_tasks` returned. In that case, we enable interrupts again and directly continue execution without executing `hlt`. + +Now our executor properly puts the CPU to sleep when there is nothing to do. We can see that the QEMU process has a much lower CPU utilization when we run our kernel using `cargo run` again. + +#### Possible Extensions + +Our executor is now able to run tasks in an efficient way. It utilizes waker notifications to avoid polling waiting tasks and puts the CPU to sleep when there is currently no work to do. However, our executor is still quite basic, and there are many possible ways to extend its functionality: + +- **Scheduling**: For our `task_queue`, we currently use the [`VecDeque`] type to implement a _first in first out_ (FIFO) strategy, which is often also called _round robin_ scheduling. This strategy might not be the most efficient for all workloads. For example, it might make sense to prioritize latency-critical tasks or tasks that do a lot of I/O. See the [scheduling chapter] of the [_Operating Systems: Three Easy Pieces_] book or the [Wikipedia article on scheduling][scheduling-wiki] for more information. +- **Task Spawning**: Our `Executor::spawn` method currently requires a `&mut self` reference and is thus no longer available after invoking the `run` method. To fix this, we could create an additional `Spawner` type that shares some kind of queue with the executor and allows task creation from within tasks themselves. The queue could be the `task_queue` directly or a separate queue that the executor checks in its run loop. +- **Utilizing Threads**: We don't have support for threads yet, but we will add it in the next post. This will make it possible to launch multiple instances of the executor in different threads. The advantage of this approach is that the delay imposed by long-running tasks can be reduced because other tasks can run concurrently. This approach also allows it to utilize multiple CPU cores. +- **Load Balancing**: When adding threading support, it becomes important to know how to distribute the tasks between the executors to ensure that all CPU cores are utilized. A common technique for this is [_work stealing_]. + +[scheduling chapter]: http://pages.cs.wisc.edu/~remzi/OSTEP/cpu-sched.pdf +[_Operating Systems: Three Easy Pieces_]: http://pages.cs.wisc.edu/~remzi/OSTEP/ +[scheduling-wiki]: https://en.wikipedia.org/wiki/Scheduling_(computing) +[_work stealing_]: https://en.wikipedia.org/wiki/Work_stealing + +## Summary + +We started this post by introducing **multitasking** and differentiating between _preemptive_ multitasking, which forcibly interrupts running tasks regularly, and _cooperative_ multitasking, which lets tasks run until they voluntarily give up control of the CPU. + +We then explored how Rust's support of **async/await** provides a language-level implementation of cooperative multitasking. Rust bases its implementation on top of the polling-based `Future` trait, which abstracts asynchronous tasks. Using async/await, it is possible to work with futures almost like with normal synchronous code. The difference is that asynchronous functions return a `Future` again, which needs to be added to an executor at some point in order to run it. + +Behind the scenes, the compiler transforms async/await code to _state machines_, with each `.await` operation corresponding to a possible pause point. By utilizing its knowledge about the program, the compiler is able to save only the minimal state for each pause point, resulting in a very small memory consumption per task. One challenge is that the generated state machines might contain _self-referential_ structs, for example when local variables of the asynchronous function reference each other. To prevent pointer invalidation, Rust uses the `Pin` type to ensure that futures cannot be moved in memory anymore after they have been polled for the first time. + +For our **implementation**, we first created a very basic executor that polls all spawned tasks in a busy loop without using the `Waker` type at all. We then showed the advantage of waker notifications by implementing an asynchronous keyboard task. The task defines a static `SCANCODE_QUEUE` using the mutex-free `ArrayQueue` type provided by the `crossbeam` crate. Instead of handling keypresses directly, the keyboard interrupt handler now puts all received scancodes in the queue and then wakes the registered `Waker` to signal that new input is available. On the receiving end, we created a `ScancodeStream` type to provide a `Future` resolving to the next scancode in the queue. This made it possible to create an asynchronous `print_keypresses` task that uses async/await to interpret and print the scancodes in the queue. + +To utilize the waker notifications of the keyboard task, we created a new `Executor` type that uses an `Arc`-shared `task_queue` for ready tasks. We implemented a `TaskWaker` type that pushes the ID of woken tasks directly to this `task_queue`, which are then polled again by the executor. To save power when no tasks are runnable, we added support for putting the CPU to sleep using the `hlt` instruction. Finally, we discussed some potential extensions to our executor, for example, providing multi-core support. + +## What's Next? + +Using async/wait, we now have basic support for cooperative multitasking in our kernel. While cooperative multitasking is very efficient, it leads to latency problems when individual tasks keep running for too long, thus preventing other tasks from running. For this reason, it makes sense to also add support for preemptive multitasking to our kernel. + +In the next post, we will introduce _threads_ as the most common form of preemptive multitasking. In addition to resolving the problem of long-running tasks, threads will also prepare us for utilizing multiple CPU cores and running untrusted user programs in the future. From a6690d6b79d985dd01a16b3ad6c3477e97a86b2b Mon Sep 17 00:00:00 2001 From: TakiMoysha <36836047+TakiMoysha@users.noreply.github.com> Date: Mon, 8 Sep 2025 06:47:32 +0200 Subject: [PATCH 2/9] translated next section --- .../posts/12-async-await/index.ru.md | 226 +++++++++--------- 1 file changed, 114 insertions(+), 112 deletions(-) diff --git a/blog/content/edition-2/posts/12-async-await/index.ru.md b/blog/content/edition-2/posts/12-async-await/index.ru.md index f2092fd1..d6f2ba5e 100644 --- a/blog/content/edition-2/posts/12-async-await/index.ru.md +++ b/blog/content/edition-2/posts/12-async-await/index.ru.md @@ -67,7 +67,7 @@ translators = ["TakiMoysha"] Недостатком вытесняющей многозадачности является то, что каждой задаче требуется собственный стек. По сравнению с общим стеком это приводит к более высокому использованию памяти на задачу и часто ограничивает количество задач в системе. Другим недостатком является то, что ОС всегда должна сохранять полное состояние регистров ЦП при каждом переключении задач, даже если задача использовала только небольшую часть регистров. -Вытесняющая многозадачность и потоки - фундаментальные компонтенты ОС, т.к. они позволяют запускать (run untrusted userspace programs ?TODO:). Мы подробнее обсудим эти концепции в будущийх постах. Однако сейчас, мы сосредоточимся на кооперативной многозадачности, которая также предоставляет полезные возможности для нашего ядра. +Вытесняющая многозадачность и потоки - фундаментальные компонтенты ОС, т.к. они позволяют запускать недоверенные программы в userspace (run untrusted userspace programs) . Мы подробнее обсудим эти концепции в будущийх постах. Однако сейчас, мы сосредоточимся на кооперативной многозадачности, которая также предоставляет полезные возможности для нашего ядра. ### Кооперативная Многозадачность @@ -176,14 +176,12 @@ let file_content = loop { Более эффективным подходом может быть _блокировка_ текущего потока до тех пор, пока футура не станет доступной. Конечно, это возможно только при наличии потоков, поэтому это решение не работает для нашего ядра, по крайней мере, пока. Даже в системах, где поддерживается блокировка, она часто нежелательна, поскольку превращает асинхронную задачу в синхронную, тем самым сдерживая потенциальные преимущества параллельных задач в плане производительности. - -#### Future Combinators - -An alternative to waiting is to use future combinators. Future combinators are methods like `map` that allow chaining and combining futures together, similar to the methods of the [`Iterator`] trait. Instead of waiting on the future, these combinators return a future themselves, which applies the mapping operation on `poll`. +#### Комбинаторы Future +Альтернативой ожиданию является использование комбинаторов future. _Комбинаторы future_ - это методы вроде `map`, которые позволяют объединять и связывать future между собой, аналогично методам трейта [`Iterator`]. Вместо того чтобы ожидать выполнения future, эти комбинаторы сами возвращают future, которые применяет операцию преобразования при вызове `poll`. [`Iterator`]: https://doc.rust-lang.org/stable/core/iter/trait.Iterator.html -As an example, a simple `string_len` combinator for converting a `Future` to a `Future` could look like this: +Например, простой комбинатор `string_len` для преобразования `Future` в `Future` может выглядеть так: ```rust struct StringLen { @@ -209,36 +207,30 @@ fn string_len(string: impl Future) } } -// Usage +// Использование fn file_len() -> impl Future { let file_content_future = async_read_file("foo.txt"); string_len(file_content_future) } ``` -This code does not quite work because it does not handle [_pinning_], but it suffices as an example. The basic idea is that the `string_len` function wraps a given `Future` instance into a new `StringLen` struct, which also implements `Future`. When the wrapped future is polled, it polls the inner future. If the value is not ready yet, `Poll::Pending` is returned from the wrapped future too. If the value is ready, the string is extracted from the `Poll::Ready` variant and its length is calculated. Afterwards, it is wrapped in `Poll::Ready` again and returned. +Этот код не совсем корректен, потому что не учитывает [_pinning_], но он подходит для примера. Основная идея в том, что функция `string_len` оборачивает переданный экземпляр `Future` в новую структуру `StringLen`, которая также реализует `Future`. При опросе обёрнутого future опрашивается внутренний future. Если значение ещё не готово, из обёрнутого future также возвращается `Poll::Pending`. Если значение готово, строка извлекается из варианта `Poll::Ready`, вычисляется её длина, после чего результат снова оборачивается в `Poll::Ready` и возвращается. [_pinning_]: https://doc.rust-lang.org/stable/core/pin/index.html -With this `string_len` function, we can calculate the length of an asynchronous string without waiting for it. Since the function returns a `Future` again, the caller can't work directly on the returned value, but needs to use combinator functions again. This way, the whole call graph becomes asynchronous and we can efficiently wait for multiple futures at once at some point, e.g., in the main function. +С помощью функции `string_len` можно вычислить длину асинхронной строки, не дожидаясь её завершения. Поскольку функция снова возвращает `Future`, вызывающий код не может работать с возвращённым значением напрямую, а должен использовать комбинаторы. Таким образом, весь граф вызовов становится асинхронным, и в какой-то момент (например, в основной функции) можно эффективно ожидать завершения нескольких future одновременно. -Because manually writing combinator functions is difficult, they are often provided by libraries. While the Rust standard library itself provides no combinator methods yet, the semi-official (and `no_std` compatible) [`futures`] crate does. Its [`FutureExt`] trait provides high-level combinator methods such as [`map`] or [`then`], which can be used to manipulate the result with arbitrary closures. +Так как ручное написание функций-комбинаторов сложно, они обычно предоставляются библиотеками. Стандартная библиотека Rust пока не содержит методов-комбинаторов, но полуофициальная (и совместимая с `no_std`) библиотека [`futures`] предоставляет их. Её трейт [`FutureExt`] включает высокоуровневые методы-комбинаторы, такие как [`map`] или [`then`], которые позволяют манипулировать результатом с помощью произвольных замыканий. -[`futures`]: https://docs.rs/futures/0.3.4/futures/ -[`FutureExt`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html -[`map`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html#method.map -[`then`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html#method.then +##### Преимущества -##### Advantages +Большое преимущество future комбинаторов (future combinators) в том, что они сохраняют асинхронность. В сочетании с асинхронными интерфейсами ввода-вывода такой подход может обеспечить очень высокую производительность. То, что future кобинаторы реализованы как обычные структуры с имплементацией трейтов, позволяет компилятору чрезвычайно оптимизировать их. Подробнее см. в посте [_Futures с нулевой стоимостью в Rust_], где было объявлено о добавлении futures в экосистему Rust. -The big advantage of future combinators is that they keep the operations asynchronous. In combination with asynchronous I/O interfaces, this approach can lead to very high performance. The fact that future combinators are implemented as normal structs with trait implementations allows the compiler to excessively optimize them. For more details, see the [_Zero-cost futures in Rust_] post, which announced the addition of futures to the Rust ecosystem. +[_Futures с нулевой стоимостью в Rust_]: https://aturon.github.io/blog/2016/08/11/futures/ -[_Zero-cost futures in Rust_]: https://aturon.github.io/blog/2016/08/11/futures/ - -##### Drawbacks - -While future combinators make it possible to write very efficient code, they can be difficult to use in some situations because of the type system and the closure-based interface. For example, consider code like this: +##### Недостатки +Хотя future комбинаторы позволяют писать очень эффективный код, их может быть сложно использовать в некоторых ситуациях из-за системы типов и интерфейса на основе замыканий. Например, рассмотрим такой код: ```rust fn example(min_len: usize) -> impl Future { async_read_file("foo.txt").then(move |content| { @@ -251,34 +243,34 @@ fn example(min_len: usize) -> impl Future { } ``` -([Try it on the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=91fc09024eecb2448a85a7ef6a97b8d8)) -Here we read the file `foo.txt` and then use the [`then`] combinator to chain a second future based on the file content. If the content length is smaller than the given `min_len`, we read a different `bar.txt` file and append it to `content` using the [`map`] combinator. Otherwise, we return only the content of `foo.txt`. +([Попробовать в песочнице](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=91fc09024eecb2448a85a7ef6a97b8d8)) -We need to use the [`move` keyword] for the closure passed to `then` because otherwise there would be a lifetime error for `min_len`. The reason for the [`Either`] wrapper is that `if` and `else` blocks must always have the same type. Since we return different future types in the blocks, we must use the wrapper type to unify them into a single type. The [`ready`] function wraps a value into a future, which is immediately ready. The function is required here because the `Either` wrapper expects that the wrapped value implements `Future`. +Здесь мы читаем файл `foo.txt`, а затем используем комбинатор [`then`], чтобы связать вторую футуру на основе содержимого файла. Если длина содержимого меньше заданного `min_len`, мы читаем другой файл `bar.txt` и добавляем его к `content` с помощью комбинатора [`map`]. В противном случае возвращаем только содержимое `foo.txt`. + +Нам нужно использовать ключевое слово [`move`] для замыкания, передаваемого в `then`, иначе возникнет ошибка времени жизни (lifetime) для `min_len`. Причина использования обёртки [`Either`] заключается в том, что блоки `if` и `else` всегда должны возвращать значения одного типа. Поскольку в блоках возвращаются разные типы будущих значений, нам необходимо использовать обёртку, чтобы привести их к единому типу. Функция [`ready`] оборачивает значение в будущее, которое сразу готово к использованию. Здесь она необходима, потому что обёртка `Either` ожидает, что обёрнутое значение реализует `Future`. [`move` keyword]: https://doc.rust-lang.org/std/keyword.move.html [`Either`]: https://docs.rs/futures/0.3.4/futures/future/enum.Either.html [`ready`]: https://docs.rs/futures/0.3.4/futures/future/fn.ready.html -As you can imagine, this can quickly lead to very complex code for larger projects. It gets especially complicated if borrowing and different lifetimes are involved. For this reason, a lot of work was invested in adding support for async/await to Rust, with the goal of making asynchronous code radically simpler to write. +Как можно догадаться, такой подход быстро приводит к очень сложному коду, особенно в крупных проектах. Ситуация ещё больше усложняется, если задействованы заимствования (borrowing) и разные времена жизни (lifetimes). Именно поэтому в Rust было вложено много усилий для добавления поддержки `async/await` — с целью сделать написание асинхронного кода радикально проще. -### The Async/Await Pattern +### Паттерн Async/Await -The idea behind async/await is to let the programmer write code that _looks_ like normal synchronous code, but is turned into asynchronous code by the compiler. It works based on the two keywords `async` and `await`. The `async` keyword can be used in a function signature to turn a synchronous function into an asynchronous function that returns a future: +Идея async/await заключается в том, чтобы позволить программисту писать код, который _выглядит_ как обычный синхронный код, но превращается в асинхронный код компилятором. Это работает на основе двух ключевых слов `async` и `await`. Ключевое слово `async` можно использовать в сигнатуре функции для превращения синхронной функции в асинхронную функцию, возвращающую future: ```rust async fn foo() -> u32 { 0 } - -// the above is roughly translated by the compiler to: +// примерно переводится компилятором в: fn foo() -> impl Future { future::ready(0) } ``` -This keyword alone wouldn't be that useful. However, inside `async` functions, the `await` keyword can be used to retrieve the asynchronous value of a future: +Одного этого ключевого слова недостаточно. Однако внутри функций `async` можно использовать ключевое слово `await`, чтобы получить асинхронное значение future: ```rust async fn example(min_len: usize) -> String { @@ -291,36 +283,48 @@ async fn example(min_len: usize) -> String { } ``` -([Try it on the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d93c28509a1c67661f31ff820281d434)) +В данном примере `async_read_file` — это асинхронная функция, возвращающая будущее строки. -This function is a direct translation of the `example` function from [above](#drawbacks) that used combinator functions. Using the `.await` operator, we can retrieve the value of a future without needing any closures or `Either` types. As a result, we can write our code like we write normal synchronous code, with the difference that _this is still asynchronous code_. +```rust +async fn example(min_len: usize) -> String { + let content = async_read_file("foo.txt").await; + if content.len() < min_len { + content + &async_read_file("bar.txt").await + } else { + content + } +} +``` -#### State Machine Transformation +([Попробовать в песочнице](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d93c28509a1c67661f31ff820281d434)) -Behind the scenes, the compiler converts the body of the `async` function into a [_state machine_], with each `.await` call representing a different state. For the above `example` function, the compiler creates a state machine with the following four states: +Эта ф-ция - прямой перевод `example` написанной [выше](#Недостатки), которая использовала комбинаторные ф-ции. Используя оператор `.await`, мы можем получить значение future без необходимости использования каких-либо замыканий или типов `Either`. В результате, мы можем писать наш код так же, как если бы это был обычный синхронный код, с той лишь разницей, что _это все еще асинхронный код_. + +#### Преобразованиe Конечных Автоматов (Машина состояний) + +За кулисами компилятор преобразует тело ф-ции `async` в [_state machine_] с каждым вызовом `.await`, представляющим собой разное состояние. Для вышеуказанной ф-ции `example`, компилятор создает state machine с четырьмя состояниями. [_state machine_]: https://en.wikipedia.org/wiki/Finite-state_machine -![Four states: start, waiting on foo.txt, waiting on bar.txt, end](async-state-machine-states.svg) +![Четыре состояния: start, waiting on foo.txt, waiting on bar.txt, end](async-state-machine-states.svg) -Each state represents a different pause point in the function. The _"Start"_ and _"End"_ states represent the function at the beginning and end of its execution. The _"Waiting on foo.txt"_ state represents that the function is currently waiting for the first `async_read_file` result. Similarly, the _"Waiting on bar.txt"_ state represents the pause point where the function is waiting on the second `async_read_file` result. +Каждое состояние представляет собой точку остановки в функции. Состояния _"Start"_ и _"End"_, указывают на начало и конец выполнения ф-ции. Состояние _"waiting on foo.txt"_ - функция в данный момент ждёт первого результата `async_read_file`. Аналогично, состояние _"waiting on bar.txt"_ представляет остановку, когда ф-ция ожидает второй результат `async_read_file`. -The state machine implements the `Future` trait by making each `poll` call a possible state transition: +Конечный автомат реализует trait `Future` делая каждый вызов `poll` возможным переход между состояниями: -![Four states and their transitions: start, waiting on foo.txt, waiting on bar.txt, end](async-state-machine-basic.svg) +![Четыре состояния и переходы: start, waiting on foo.txt, waiting on bar.txt, end](async-state-machine-basic.svg) -The diagram uses arrows to represent state switches and diamond shapes to represent alternative ways. For example, if the `foo.txt` file is not ready, the path marked with _"no"_ is taken and the _"Waiting on foo.txt"_ state is reached. Otherwise, the _"yes"_ path is taken. The small red diamond without a caption represents the `if content.len() < 100` branch of the `example` function. +Диаграмма использует стрелки для представления переключений состояний и ромбы для представления альтернативных путей. Например, если файл `foo.txt` не готов, то мы используется путь _"no"_ переходя в состояние _"waiting on foo.txt"_. Иначе, используется путь _"да"_. Где маленький красный комб без подписи - ветвь ф-ции exmple, где `if content.len() < 100`. -We see that the first `poll` call starts the function and lets it run until it reaches a future that is not ready yet. If all futures on the path are ready, the function can run till the _"End"_ state, where it returns its result wrapped in `Poll::Ready`. Otherwise, the state machine enters a waiting state and returns `Poll::Pending`. On the next `poll` call, the state machine then starts from the last waiting state and retries the last operation. +Мы видим, что первый вызов `poll` запускает функцию и она выполняться до тех пор, пока у футуры не будет результата. Если все футуры на пути готовы, ф-ция может выполниться до состояния _"end"_ , то есть вернуть свой результат, завернутый в `Poll::Ready`. В противном случае конечный автомат переходит в состояние ожидания и возвращает `Poll::Pending`. При следующем вызове `poll` машина состояний начинает с последнего состояния ожидания и повторяет последнюю операцию. -#### Saving State +#### Сохранение состояния -In order to be able to continue from the last waiting state, the state machine must keep track of the current state internally. In addition, it must save all the variables that it needs to continue execution on the next `poll` call. This is where the compiler can really shine: Since it knows which variables are used when, it can automatically generate structs with exactly the variables that are needed. - -As an example, the compiler generates structs like the following for the above `example` function: +Для продолжнеия работы с последнего состояния ожидания, автомат должен отслеживать текущее состояние внутри себя. Еще, он должен сохранять все переменные, которые необходимы для продолжнеия выполнения при следующем вызове `poll`. Здесь компилятор действительно может проявить себя: зная, когда используются те или иные переменные, он может автоматически создавать структуры с точным набором требуемых переменных. +Например, компилятор генерирует структуры для вышеприведенной ф-ции `example`: ```rust -// The `example` function again so that you don't have to scroll up +// снова `example` что бы вам не пришлось прокручивать вверх async fn example(min_len: usize) -> String { let content = async_read_file("foo.txt").await; if content.len() < min_len { @@ -330,7 +334,7 @@ async fn example(min_len: usize) -> String { } } -// The compiler-generated state structs: +// компиялтор генерирует структуры struct StartState { min_len: usize, @@ -349,15 +353,15 @@ struct WaitingOnBarTxtState { struct EndState {} ``` -In the "start" and _"Waiting on foo.txt"_ states, the `min_len` parameter needs to be stored for the later comparison with `content.len()`. The _"Waiting on foo.txt"_ state additionally stores a `foo_txt_future`, which represents the future returned by the `async_read_file` call. This future needs to be polled again when the state machine continues, so it needs to be saved. +В состояниях "start" и _"waiting on foo.txt"_ необходимо сохранить параметр `min_len` для последующего сравнения с `content.len()`. Состояние _"waiting on foo.txt"_ дополнительно содержит `foo_txt_future`, представляющий future возвращаемое вызовом `async_read_file`. Этe футуру нужно опросить снова, когда автомат продолжит свою работу, поэтому его нужно сохранить. -The _"Waiting on bar.txt"_ state contains the `content` variable for the later string concatenation when `bar.txt` is ready. It also stores a `bar_txt_future` that represents the in-progress load of `bar.txt`. The struct does not contain the `min_len` variable because it is no longer needed after the `content.len()` comparison. In the _"end"_ state, no variables are stored because the function has already run to completion. +Состояние "waiting on bar.txt" содержит переменную `content` для последующей конкатенации строк при загрузке файла `bar.txt`. Оно также хранит `bar_txt_future`, представляющее текущую загрузку файла `bar.txt`. Эта структура не содержит переменную `min_len`, потому что она уже не нужна после проверки длины строки `content.len()`. В состоянии _"end"_, в структуре ничего нет, т.к. ф-ция завершилась полностью. -Keep in mind that this is only an example of the code that the compiler could generate. The struct names and the field layout are implementation details and might be different. +Учтите, что приведенный здесь код - это только пример того, какая структура может быть сгенерирована компилятором Имена структур и расположение полей - детали реализации и могут отличаться. -#### The Full State Machine Type +#### Полный Конечный Автомат -While the exact compiler-generated code is an implementation detail, it helps in understanding to imagine how the generated state machine _could_ look for the `example` function. We already defined the structs representing the different states and containing the required variables. To create a state machine on top of them, we can combine them into an [`enum`]: +При этом точно сгенерированный код компилятора является деталью реализации, это помогает понять, представив, как могла бы выглядеть машина состояний для функции `example`. Мы уже определили структуры, представляющие разные состояния и содержащие необходимые переменные. Чтобы создать машину состояний на их основе, мы можем объединить их в [`enum`]: [`enum`]: https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html @@ -370,11 +374,11 @@ enum ExampleStateMachine { } ``` -We define a separate enum variant for each state and add the corresponding state struct to each variant as a field. To implement the state transitions, the compiler generates an implementation of the `Future` trait based on the `example` function: +Мы определяем отдельный вариант перечисления (enum) для каждого состояния и добавляем соответствующую структуру состояния в каждый вариант как поле. Чтобы реализовать переходы между состояниями, компилятор генерирует реализацию trait'а `Future` на основе функции `example`: ```rust impl Future for ExampleStateMachine { - type Output = String; // return type of `example` + type Output = String; // возвращает тип из `example` fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { loop { @@ -389,17 +393,17 @@ impl Future for ExampleStateMachine { } ``` -The `Output` type of the future is `String` because it's the return type of the `example` function. To implement the `poll` function, we use a `match` statement on the current state inside a `loop`. The idea is that we switch to the next state as long as possible and use an explicit `return Poll::Pending` when we can't continue. +Тип `Output` будущего равен `String`, потому что это тип возвращаемого значения функции `example`. Для реализации метода `poll` мы используем условную инструкцию `match` на текущем состоянии внутри цикла. Идея в том, что мы переходим к следующему состоянию, пока это возможно, и явно возвращаем `Poll::Pending`, когда мы не можем продолжить. -For simplicity, we only show simplified code and don't handle [pinning][_pinning_], ownership, lifetimes, etc. So this and the following code should be treated as pseudo-code and not used directly. Of course, the real compiler-generated code handles everything correctly, albeit possibly in a different way. +Для упрощения мы представляем только упрощенный код и не обрабатываем [закрепление][_pinning_], владения, lifetimes, и т.д. Поэтому этот и следующий код должны быть восприняты как псевдокод и не использоваться напрямую. Конечно, реальный генерируемый компилятором код обрабатывает всё верно, хотя возможно это будет сделано по-другому. -To keep the code excerpts small, we present the code for each `match` arm separately. Let's begin with the `Start` state: +Чтобы сохранить примеры кода маленькими, мы представляем код для каждого варианта `match` отдельно. Начнем с состояния `Start`: ```rust ExampleStateMachine::Start(state) => { - // from body of `example` + // из тела `example` let foo_txt_future = async_read_file("foo.txt"); - // `.await` operation + // операция`.await` let state = WaitingOnFooTxtState { min_len: state.min_len, foo_txt_future, @@ -407,20 +411,19 @@ ExampleStateMachine::Start(state) => { *self = ExampleStateMachine::WaitingOnFooTxt(state); } ``` +Машина состояний находится в состоянии `Start`, когда она прямо в начале функции. В этом случае выполняем весь код из тела функции `example` до первого `.await`. Чтобы обработать операцию `.await`, мы меняем состояние машины на `WaitingOnFooTxt`, которое включает в себя построение структуры `WaitingOnFooTxtState`. -The state machine is in the `Start` state when it is right at the beginning of the function. In this case, we execute all the code from the body of the `example` function until the first `.await`. To handle the `.await` operation, we change the state of the `self` state machine to `WaitingOnFooTxt`, which includes the construction of the `WaitingOnFooTxtState` struct. - -Since the `match self {…}` statement is executed in a loop, the execution jumps to the `WaitingOnFooTxt` arm next: +Пока `match self {…}` выполняется в цилке, выполнение прыгает к `WaitingOnFooTxt`: ```rust ExampleStateMachine::WaitingOnFooTxt(state) => { match state.foo_txt_future.poll(cx) { Poll::Pending => return Poll::Pending, Poll::Ready(content) => { - // from body of `example` + // из тела `example` if content.len() < state.min_len { let bar_txt_future = async_read_file("bar.txt"); - // `.await` operation + // операция `.await` let state = WaitingOnBarTxtState { content, bar_txt_future, @@ -435,13 +438,13 @@ ExampleStateMachine::WaitingOnFooTxt(state) => { } ``` -In this `match` arm, we first call the `poll` function of the `foo_txt_future`. If it is not ready, we exit the loop and return `Poll::Pending`. Since `self` stays in the `WaitingOnFooTxt` state in this case, the next `poll` call on the state machine will enter the same `match` arm and retry polling the `foo_txt_future`. +В этом варианте `match`, вначале мы вызываем функцию `poll` для `foo_txt_future`. Если она не готова, мы выходим из цикла и возвращаем `Poll::Pending`. В этом случае `self` остается в состоянии `WaitingOnFooTxt`, следующий вызов функции `poll` на машине состояний попадёт в тот же `match` и повторит проверку готовности `foo_txt_future`. -When the `foo_txt_future` is ready, we assign the result to the `content` variable and continue to execute the code of the `example` function: If `content.len()` is smaller than the `min_len` saved in the state struct, the `bar.txt` file is read asynchronously. We again translate the `.await` operation into a state change, this time into the `WaitingOnBarTxt` state. Since we're executing the `match` inside a loop, the execution directly jumps to the `match` arm for the new state afterward, where the `bar_txt_future` is polled. +Когда `foo_txt_future` готов, мы присваиваем результат переменной `content` и продолжаем выполнять код функции `example`: Если `content.len()` меньше сохранённого в структуре состояния `min_len`, файл `bar.txt` читается асинхронно. Мы ещё раз переводим операцию `.await` в изменение состояния, теперь в состояние `WaitingOnBarTxt`. Следуя за выполнением `match` внутри цикла, выполнение прямо переходит к варианту `match` для нового состояния позже, где проверяется готовность `bar_txt_future`. -In case we enter the `else` branch, no further `.await` operation occurs. We reach the end of the function and return `content` wrapped in `Poll::Ready`. We also change the current state to the `End` state. +В случае входа в ветку `else`, более никаких операций `.await` не происходит. Мы достигаем конца функции и возвращаем `content` обёрнутую в `Poll::Ready`. Также меняем текущее состояние на `End`. -The code for the `WaitingOnBarTxt` state looks like this: +Код для состояния `WaitingOnBarTxt` выглядит следующим образом: ```rust ExampleStateMachine::WaitingOnBarTxt(state) => { @@ -449,36 +452,34 @@ ExampleStateMachine::WaitingOnBarTxt(state) => { Poll::Pending => return Poll::Pending, Poll::Ready(bar_txt) => { *self = ExampleStateMachine::End(EndState); - // from body of `example` + // из тела `example` return Poll::Ready(state.content + &bar_txt); } } } ``` -Similar to the `WaitingOnFooTxt` state, we start by polling the `bar_txt_future`. If it is still pending, we exit the loop and return `Poll::Pending`. Otherwise, we can perform the last operation of the `example` function: concatenating the `content` variable with the result from the future. We update the state machine to the `End` state and then return the result wrapped in `Poll::Ready`. - -Finally, the code for the `End` state looks like this: +Аналогично состоянию `WaitingOnFooTxt`, мы начинаем с проверки готовности `bar_txt_future`. Если она ещё не готова, мы выходим из цикла и возвращаем `Poll::Pending`. В противном случае, мы можем выполнить последнюю операцию функции `example`: конкатенацию переменной `content` с результатом футуры. Обновляем машину состояний в состояние `End` и затем возвращаем результат обёрнутый в `Poll::Ready`. +В итоге, код для `End` состояния выглядит так: ```rust ExampleStateMachine::End(_) => { - panic!("poll called after Poll::Ready was returned"); + panic!("poll вызван после возврата Poll::Ready"); } ``` +Футуры не должны повторно проверяться после того, как они вернули `Poll::Ready`, поэтому паникуем, если вызвана функция `poll`, когда мы уже находимся в состоянии `End`. -Futures should not be polled again after they returned `Poll::Ready`, so we panic if `poll` is called while we are already in the `End` state. +Теперь мы знаем, что сгенерированная машина состояний и ее реализация интерфейса `Future` _могла бы_ выглядеть так. На практике компилятор генерирует код по-другому. (Если вас заинтересует, то реализация ныне основана на [_корутинах_], но это только деталь имплементации.) -We now know what the compiler-generated state machine and its implementation of the `Future` trait _could_ look like. In practice, the compiler generates code in a different way. (In case you're interested, the implementation is currently based on [_coroutines_], but this is only an implementation detail.) +[_корутинах_]: https://doc.rust-lang.org/stable/unstable-book/language-features/coroutines.html -[_coroutines_]: https://doc.rust-lang.org/stable/unstable-book/language-features/coroutines.html - -The last piece of the puzzle is the generated code for the `example` function itself. Remember, the function header was defined like this: +Последняя часть загадки – сгенерированный код для самой функции `example`. Помните, что заголовок функции был определён следующим образом: ```rust async fn example(min_len: usize) -> String ``` -Since the complete function body is now implemented by the state machine, the only thing that the function needs to do is to initialize the state machine and return it. The generated code for this could look like this: +Теперь, когда весь функционал реализуется машиной состояний, единственное, что ф-ция должна сделать - это инициализировать эту машику и вернуть ее. Сгенерированный код для этого может выглядеть следующим образом: ```rust fn example(min_len: usize) -> ExampleStateMachine { @@ -488,17 +489,18 @@ fn example(min_len: usize) -> ExampleStateMachine { } ``` -The function no longer has an `async` modifier since it now explicitly returns an `ExampleStateMachine` type, which implements the `Future` trait. As expected, the state machine is constructed in the `Start` state and the corresponding state struct is initialized with the `min_len` parameter. +Функция больше не имеет модификатора `async`, поскольку теперь явно возвращает тип `ExampleStateMachine`, который реализует трейт `Future`. Как ожидалось, машина состояний создается в состоянии `start` и соответствующая ему структура состояния инициализируется параметром `min_len`. -Note that this function does not start the execution of the state machine. This is a fundamental design decision of futures in Rust: they do nothing until they are polled for the first time. +Заметьте, что эта функция не запускает выполнение машины состояний. Это фундаментальное архитектурное решение для футур в Rust: они ничего не делают, пока не будет произведена первая проверка на готовность. -### Pinning +#### Закрепление (Pinning) -We already stumbled across _pinning_ multiple times in this post. Now is finally the time to explore what pinning is and why it is needed. +Мы уже несколько раз столкнулись с понятием _закрепления_ (pinnig) в этом посте. Наконец, время чтобы изучить, что такое закрепление и почему оно необходимо. -#### Self-Referential Structs -As explained above, the state machine transformation stores the local variables of each pause point in a struct. For small examples like our `example` function, this was straightforward and did not lead to any problems. However, things become more difficult when variables reference each other. For example, consider this function: +#### Самоссылающиеся структуры + +Как объяснялось выше, переходы конечных автоматов хранят локальные переменные для каждой точки остановки в структуре. Для простых примеров, как наш `example` функции, это было просто и не привело к никаким проблемам. Однако делаются сложнее, когда переменные ссылаются друг на друга. Например, рассмотрите следующую функцию: ```rust async fn pin_example() -> i32 { @@ -509,50 +511,50 @@ async fn pin_example() -> i32 { } ``` -This function creates a small `array` with the contents `1`, `2`, and `3`. It then creates a reference to the last array element and stores it in an `element` variable. Next, it asynchronously writes the number converted to a string to a `foo.txt` file. Finally, it returns the number referenced by `element`. +Эта функция создает маленький `array` с содержимым `1`, `2`, и `3`. Затем она создает ссылку на последний элемент массива и хранит ее в переменной `element`. Далее, асинхронно записывает число, преобразованное в строку, в файл `foo.txt`. В конце, возвращает число, ссылка на которое хранится в `element`. -Since the function uses a single `await` operation, the resulting state machine has three states: start, end, and "waiting on write". The function takes no arguments, so the struct for the start state is empty. Like before, the struct for the end state is empty because the function is finished at this point. The struct for the "waiting on write" state is more interesting: +Следуя своей единственной операции `await`, машина состояний состоит из трех состояний: start, end и "waiting on write". Функция не принимает аргументов, поэтому структура для начального состояния пуста. Как обычно, структура для конечного состояния также пустая, поскольку функция завершена на этом этапе. Структура для "waiting on write" более интересна: ```rust struct WaitingOnWriteState { array: [1, 2, 3], - element: 0x1001c, // address of the last array element + element: 0x1001c, // адрес последнего элемента в array } ``` -We need to store both the `array` and `element` variables because `element` is required for the return value and `array` is referenced by `element`. Since `element` is a reference, it stores a _pointer_ (i.e., a memory address) to the referenced element. We used `0x1001c` as an example memory address here. In reality, it needs to be the address of the last element of the `array` field, so it depends on where the struct lives in memory. Structs with such internal pointers are called _self-referential_ structs because they reference themselves from one of their fields. +Мы должны хранить как `array`, так и `element` потому что `element` требуется для значения возврата, а `array` ссылается на `element`. Следовательно, `element` является _указателем_ (pointer) (адресом памяти), который хранит адрес ссылаемого элемента. В этом примере мы использовали `0x1001c` в качестве примера адреса, в реальности он должен быть адресом последнего элемента поля `array`, что зависит от места расположения структуры в памяти. Структуры с такими внутренними указателями называются _самоссылочными_ (self-referential) структурами, потому что они ссылаются на себя из одного из своих полей. -#### The Problem with Self-Referential Structs +#### Проблемы с Самоссылочными Структурами -The internal pointer of our self-referential struct leads to a fundamental problem, which becomes apparent when we look at its memory layout: +Внутренний указатель нашей самоссылочной структуры приводит к базовой проблеме, которая становится очевидной, когда мы посмотрим на её раскладку памяти: -![array at 0x10014 with fields 1, 2, and 3; element at address 0x10020, pointing to the last array element at 0x1001c](self-referential-struct.svg) +![массив от 0x10014 с полями 1, 2, и 3; элемент в адресе 0x10020, указывающий на последний массив-элемент в 0x1001c](self-referential-struct.svg) -The `array` field starts at address 0x10014 and the `element` field at address 0x10020. It points to address 0x1001c because the last array element lives at this address. At this point, everything is still fine. However, an issue occurs when we move this struct to a different memory address: +Поле `array` начинается в адресе `0x10014`, а поле `element` - в адресе `0x10020`. Оно указывает на адрес `0x1001c`, потому что последний элемент массива находится там. В этот момент все ещё в порядке. Однако проблема возникает, когда мы перемещаем эту структуру на другой адрес памяти: -![array at 0x10024 with fields 1, 2, and 3; element at address 0x10030, still pointing to 0x1001c, even though the last array element now lives at 0x1002c](self-referential-struct-moved.svg) +![массив от 0x10024 с полями 1, 2, и 3; элемент в адресе 0x10030, продолжающий указывать на 0x1001c, хотя последний массив-элемент сейчас находится в 0x1002c](self-referential-struct-moved.svg) -We moved the struct a bit so that it starts at address `0x10024` now. This could, for example, happen when we pass the struct as a function argument or assign it to a different stack variable. The problem is that the `element` field still points to address `0x1001c` even though the last `array` element now lives at address `0x1002c`. Thus, the pointer is dangling, with the result that undefined behavior occurs on the next `poll` call. +Мы переместили структуру немного так, чтобы она теперь начиналась в адресе `0x10024`. Это могло произойти, например, когда мы передаем структуру как аргумент функции или присваиваем ей другое переменной стека. Проблема заключается в том, что поле `element` все ещё указывает на адрес `0x1001c`, хотя последний элемент массива теперь находится в адресе `0x1002c`. Поэтому указатель висит, с результатом неопределённого поведения на следующем вызове `poll`. -#### Possible Solutions +#### Возможные решения -There are three fundamental approaches to solving the dangling pointer problem: +Существует три основных подхода к решению проблемы висящих указателей (dangling pointers): -- **Update the pointer on move:** The idea is to update the internal pointer whenever the struct is moved in memory so that it is still valid after the move. Unfortunately, this approach would require extensive changes to Rust that would result in potentially huge performance losses. The reason is that some kind of runtime would need to keep track of the type of all struct fields and check on every move operation whether a pointer update is required. -- **Store an offset instead of self-references:**: To avoid the requirement for updating pointers, the compiler could try to store self-references as offsets from the struct's beginning instead. For example, the `element` field of the above `WaitingOnWriteState` struct could be stored in the form of an `element_offset` field with a value of 8 because the array element that the reference points to starts 8 bytes after the struct's beginning. Since the offset stays the same when the struct is moved, no field updates are required. +- **Обновление указателя при перемещении**: Идея состоит в обновлении внутреннего указателя при каждом перемещении структуры в памяти, чтобы она оставалась действительной после перемещения. Однако этот подход требует значительных изменений в Rust, которые могут привести к потенциальным значительным потерям производительности. Причина заключается в том, что необходимо каким-то образом отслеживать тип всех полей структуры и проверять на каждом операции перемещения, требуется ли обновление указателя. +- **Хранение смещения (offset) вместо самоссылающихся ссылок**: Чтобы избежать необходимости обновления указателей, компилятор мог бы попытаться хранить самоссы ссылки в форме смещений от начала структуры вместо прямых ссылок. Например, поле `element` вышеупомянутой `WaitingOnWriteState` структуры можно было бы хранить в виде поля `element_offset` c значением 8, потому что элемент массива, на который указывает ссылка, находится за 8 байтов после начала структуры. Смещение остается неизменным при перемещении структуры, так что не требуются обновления полей. + Проблема с этим подходом в том, что требуется, чтобы компилятор обнаружил всех самоссылок. Это невозможно на этапе компилящии потому, что значение ссылки может зависеть от ввода пользователя, так что нам потребуется система анализа ссылок и корректная генерация состояния для структур во время исполнения. Это приведёт к дополнительным расходам времени на выполнение, а также предотвратит определённые оптимизации компилятора, что приведёт к еще большим потерям производительности. +- **Запретить перемещать структуру**: Мы увидели выше, что висящий указатель возникает только при перемещении структуры в памяти. Запретив все операции перемещения для самоссылающихся структур, можно избежать этой проблемы. Большое преимущество этого подхода состоит в том, что он можно реализовать на уровне системы типов без дополнительных расходов времени выполнения. Недостаток заключается в том, что оно возлагает на программиста обязанности по обработке перемещений самоссылающихся структур. - The problem with this approach is that it requires the compiler to detect all self-references. This is not possible at compile-time because the value of a reference might depend on user input, so we would need a runtime system again to analyze references and correctly create the state structs. This would not only result in runtime costs but also prevent certain compiler optimizations, so that it would cause large performance losses again. -- **Forbid moving the struct:** As we saw above, the dangling pointer only occurs when we move the struct in memory. By completely forbidding move operations on self-referential structs, the problem can also be avoided. The big advantage of this approach is that it can be implemented at the type system level without additional runtime costs. The drawback is that it puts the burden of dealing with move operations on possibly self-referential structs on the programmer. +Rust выбрал третий подход из-за принципа предоставления _бесплатных абстракций_ (zero cost abstractions), что означает, что абстракции не должны накладывать дополнительные расходы времени выполнения. API [_pinning_] предлагалось для решения этой проблемы в RFC 2349 (). В следующем разделе мы дадим краткий обзор этого API и объясним, как оно работает с async/await и futures. -Rust chose the third solution because of its principle of providing _zero cost abstractions_, which means that abstractions should not impose additional runtime costs. The [_pinning_] API was proposed for this purpose in [RFC 2349](https://github.com/rust-lang/rfcs/blob/master/text/2349-pin.md). In the following, we will give a short overview of this API and explain how it works with async/await and futures. + +#### Значения на Куче (Heap) -#### Heap Values - -The first observation is that [heap-allocated] values already have a fixed memory address most of the time. They are created using a call to `allocate` and then referenced by a pointer type such as `Box`. While moving the pointer type is possible, the heap value that the pointer points to stays at the same memory address until it is freed through a `deallocate` call again. +Первый наблюдение состоит в том, что значения, выделенные на [куче], обычно имеют фиксированный адрес памяти. Они создаются с помощью вызова `allocate` и затем ссылаются на тип указателя, такой как `Box`. Хотя перемещение указательного типа возможно, значение кучи, которое указывает на него, остается в том же адресе памяти до тех пор, пока оно не будет освобождено с помощью вызова `deallocate` еще раз. [heap-allocated]: @/edition-2/posts/10-heap-allocation/index.md -Using heap allocation, we can try to create a self-referential struct: +Используя аллокацию по куче, можно попытаться создать самоссылающуюся структуру: ```rust fn main() { @@ -570,15 +572,15 @@ struct SelfReferential { } ``` -([Try it on the playground][playground-self-ref]) +([Попробовать в песочнице][playground-self-ref]) [playground-self-ref]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ce1aff3a37fcc1c8188eeaf0f39c97e8 -We create a simple struct named `SelfReferential` that contains a single pointer field. First, we initialize this struct with a null pointer and then allocate it on the heap using `Box::new`. We then determine the memory address of the heap-allocated struct and store it in a `ptr` variable. Finally, we make the struct self-referential by assigning the `ptr` variable to the `self_ptr` field. +Мы создаем простую структуру с названием `SelfReferential`, которая содержит только одно поле c указателем. Во-первых, мы инициализируем эту структуру с пустым указателем и затем выделяем ее на куче с помощью `Box::new`. Затем мы определяем адрес кучи для выделенной структуры и храним его в переменной `ptr`. В конце концов, мы делаем структуру самоссылающейся, назначив переменную `ptr` полю `self_ptr`. -When we execute this code [on the playground][playground-self-ref], we see that the address of the heap value and its internal pointer are equal, which means that the `self_ptr` field is a valid self-reference. Since the `heap_value` variable is only a pointer, moving it (e.g., by passing it to a function) does not change the address of the struct itself, so the `self_ptr` stays valid even if the pointer is moved. +Когда мы запускаем этот код в [песочнице][playground-self-ref], мы видим, что адрес на куче и внутренний указатель равны, что означает, что поле `self_ptr` валидное. Поскольку переменная `heap_value` является только указателем, перемещение его (например, передачей в функцию) не изменяет адрес самой структуры, поэтому `self_ptr` остается действительным даже при перемещении указателя. -However, there is still a way to break this example: We can move out of a `Box` or replace its content: +Тем не менее, все еще есть путь сломать этот пример: мы можем выйти из `Box` или изменить содержимое: ```rust let stack_value = mem::replace(&mut *heap_value, SelfReferential { @@ -588,13 +590,13 @@ println!("value at: {:p}", &stack_value); println!("internal reference: {:p}", stack_value.self_ptr); ``` -([Try it on the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e160ee8a64cba4cebc1c0473dcecb7c8)) +([Попробовать в песочнице](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e160ee8a64cba4cebc1c0473dcecb7c8)) -Here we use the [`mem::replace`] function to replace the heap-allocated value with a new struct instance. This allows us to move the original `heap_value` to the stack, while the `self_ptr` field of the struct is now a dangling pointer that still points to the old heap address. When you try to run the example on the playground, you see that the printed _"value at:"_ and _"internal reference:"_ lines indeed show different pointers. So heap allocating a value is not enough to make self-references safe. +Мы используем функцию [`mem::replace`], чтобы заменить значение, выделенное в куче, новым экземпляром структуры. Это позволяет нам переместить исходное значение `heap_value` в стек, в то время как поле `self_ptr` структуры теперь является висящим указателем, который по-прежнему указывает на старый адрес в куче. Когда вы запустите пример в песочнице, вы увидите, что строки _«value at:»_ и _«internal reference:»_, показывают разные указатели. Таким образом, выделение значения в куче недостаточно для обеспечения безопасности самоссылок. [`mem::replace`]: https://doc.rust-lang.org/nightly/core/mem/fn.replace.html -The fundamental problem that allowed the above breakage is that `Box` allows us to get a `&mut T` reference to the heap-allocated value. This `&mut` reference makes it possible to use methods like [`mem::replace`] or [`mem::swap`] to invalidate the heap-allocated value. To resolve this problem, we must prevent `&mut` references to self-referential structs from being created. +Основная проблема, которая привела к вышеуказанной ошибке, заключается в том, что `Box` позволяет нам получить ссылку `&mut T` на значение, выделенное в куче. Эта ссылка `&mut` позволяет использовать такие методы, как [`mem::replace`] или [`mem::swap`], для аннулирования значения, выделенного в куче. Чтобы решить эту проблему, мы должны предотвратить создание ссылок `&mut` на самореференциальные структуры. [`mem::swap`]: https://doc.rust-lang.org/nightly/core/mem/fn.swap.html From 99460fcfb612490189389017a9422e4ebba6cb86 Mon Sep 17 00:00:00 2001 From: TakiMoysha <36836047+TakiMoysha@users.noreply.github.com> Date: Sat, 13 Sep 2025 01:06:06 +0200 Subject: [PATCH 3/9] translated next section --- .../posts/12-async-await/index.ru.md | 192 +++++++++--------- 1 file changed, 99 insertions(+), 93 deletions(-) diff --git a/blog/content/edition-2/posts/12-async-await/index.ru.md b/blog/content/edition-2/posts/12-async-await/index.ru.md index d6f2ba5e..25c51742 100644 --- a/blog/content/edition-2/posts/12-async-await/index.ru.md +++ b/blog/content/edition-2/posts/12-async-await/index.ru.md @@ -154,13 +154,13 @@ pub enum Poll { [`Waker`]: https://doc.rust-lang.org/nightly/core/task/struct.Waker.html -### Работа с Futures +### Working with Futures Теперь мы знаем, как определяются футуры, и понимаем основную идею метода `poll`. Однако мы все еще не знаем, как эффективно работать с футурами. Проблема в том, что они представляют собой результаты асинхронных задач, которые могут быть еще недоступны. На практике, однако, нам часто нужны эти значения непосредственно для дальнейших вычислений. Поэтому возникает вопрос: как мы можем эффективно получить значение, когда оно нам нужно? -#### Ожидание Futures +#### Waiting on Futures -Один из возможных ответов — дождаться, пока будущее станет реальностью. Это может выглядеть примерно так: +Один из возможных ответов — дождаться, пока футура исполнится. Это может выглядеть примерно так: ```rust let future = async_read_file("foo.txt"); @@ -493,10 +493,12 @@ fn example(min_len: usize) -> ExampleStateMachine { Заметьте, что эта функция не запускает выполнение машины состояний. Это фундаментальное архитектурное решение для футур в Rust: они ничего не делают, пока не будет произведена первая проверка на готовность. -#### Закрепление (Pinning) +#### Pinning +> [!note] Закрепление (pinning, пиннинг) -Мы уже несколько раз столкнулись с понятием _закрепления_ (pinnig) в этом посте. Наконец, время чтобы изучить, что такое закрепление и почему оно необходимо. +Мы уже несколько раз столкнулись с понятием _закрепления_ (pinnig, пиннинг) в этом посте. Наконец, время чтобы изучить, что такое закрепление и почему оно необходимо. +> [!note] pinning - механизм, который гарантирует, что объект в памяти не будет перемещен. #### Самоссылающиеся структуры @@ -547,14 +549,13 @@ struct WaitingOnWriteState { Rust выбрал третий подход из-за принципа предоставления _бесплатных абстракций_ (zero cost abstractions), что означает, что абстракции не должны накладывать дополнительные расходы времени выполнения. API [_pinning_] предлагалось для решения этой проблемы в RFC 2349 (). В следующем разделе мы дадим краткий обзор этого API и объясним, как оно работает с async/await и futures. - #### Значения на Куче (Heap) Первый наблюдение состоит в том, что значения, выделенные на [куче], обычно имеют фиксированный адрес памяти. Они создаются с помощью вызова `allocate` и затем ссылаются на тип указателя, такой как `Box`. Хотя перемещение указательного типа возможно, значение кучи, которое указывает на него, остается в том же адресе памяти до тех пор, пока оно не будет освобождено с помощью вызова `deallocate` еще раз. [heap-allocated]: @/edition-2/posts/10-heap-allocation/index.md -Используя аллокацию по куче, можно попытаться создать самоссылающуюся структуру: +Используя аллокацию на куче, можно попытаться создать самоссылающуюся структуру: ```rust fn main() { @@ -600,9 +601,9 @@ println!("internal reference: {:p}", stack_value.self_ptr); [`mem::swap`]: https://doc.rust-lang.org/nightly/core/mem/fn.swap.html -#### `Pin>` and `Unpin` +#### `Pin>` и `Unpin` -The pinning API provides a solution to the `&mut T` problem in the form of the [`Pin`] wrapper type and the [`Unpin`] marker trait. The idea behind these types is to gate all methods of `Pin` that can be used to get `&mut` references to the wrapped value (e.g. [`get_mut`][pin-get-mut] or [`deref_mut`][pin-deref-mut]) on the `Unpin` trait. The `Unpin` trait is an [_auto trait_], which is automatically implemented for all types except those that explicitly opt-out. By making self-referential structs opt-out of `Unpin`, there is no (safe) way to get a `&mut T` from a `Pin>` type for them. As a result, their internal self-references are guaranteed to stay valid. +API _закрепления_ предоставляет решение проблемы `&mut T` в виде типа-обертки [`Pin`] и трейта-маркера [`Unpin`]. Идея использования - ограничить все методы `Pin`, которые могут быть использованы для получения ссылок `&mut` на обернутое значение (например, [`get_mut`][pin-get-mut] или [`deref_mut`][pin-deref-mut]), на трейт `Unpin`. Трейт `Unpin` является _авто трейтом_ ([_auto trait_]), который автоматически реализуется для всех типов, за исключением тех, которые явно отказываются от него. Заставляя самореференциальные структуры отказаться от `Unpin`, не остается (безопасного) способа получить `&mut T` из типа `Pin>` для них. В результате их внутренние самореференции гарантированно остаются действительными. [`Pin`]: https://doc.rust-lang.org/stable/core/pin/struct.Pin.html [`Unpin`]: https://doc.rust-lang.org/nightly/std/marker/trait.Unpin.html @@ -610,7 +611,7 @@ The pinning API provides a solution to the `&mut T` problem in the form of the [ [pin-deref-mut]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.deref_mut [_auto trait_]: https://doc.rust-lang.org/reference/special-types-and-traits.html#auto-traits -As an example, let's update the `SelfReferential` type from above to opt-out of `Unpin`: +Как пример обновим тип `SelfReferential` тип из примера выше, что бы отказаться от `Unpin`: ```rust use core::marker::PhantomPinned; @@ -621,11 +622,11 @@ struct SelfReferential { } ``` -We opt-out by adding a second `_pin` field of type [`PhantomPinned`]. This type is a zero-sized marker type whose only purpose is to _not_ implement the `Unpin` trait. Because of the way [auto traits][_auto trait_] work, a single field that is not `Unpin` suffices to make the complete struct opt-out of `Unpin`. +Мы отказываемся от `Unpin`, добавляя второе поле `_pin` типа [`PhantomPinned`]. Этот тип является маркерным типом нулевого размера, единственной целью которого является _отказ_ от реализации трейта `Unpin`. Из-за того, как работают [_авто трейты_], одного поля, которое не является `Unpin`, достаточно, чтобы полностью исключить структуру из `Unpin`. [`PhantomPinned`]: https://doc.rust-lang.org/nightly/core/marker/struct.PhantomPinned.html -The second step is to change the `Box` type in the example to a `Pin>` type. The easiest way to do this is to use the [`Box::pin`] function instead of [`Box::new`] for creating the heap-allocated value: +Второй шаг — изменить тип `Box` в примере на `Pin>`. Самый простой способ сделать это — использовать функцию [`Box::pin`] вместо [`Box::new`] для создания значения, размещаемого в куче: [`Box::pin`]: https://doc.rust-lang.org/nightly/alloc/boxed/struct.Box.html#method.pin [`Box::new`]: https://doc.rust-lang.org/nightly/alloc/boxed/struct.Box.html#method.new @@ -637,9 +638,9 @@ let mut heap_value = Box::pin(SelfReferential { }); ``` -In addition to changing `Box::new` to `Box::pin`, we also need to add the new `_pin` field in the struct initializer. Since `PhantomPinned` is a zero-sized type, we only need its type name to initialize it. +В дополнение к изменению `Box::new` на `Box::pin`, нам также нужно добавить новое поле `_pin` в инициализатор структуры. Т.к. `PhantomPinned` является типом нулевого размера, нам нужно только его имя типа для инициализации. -When we [try to run our adjusted example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=961b0db194bbe851ff4d0ed08d3bd98a) now, we see that it no longer works: +Когда мы [попробуем запустить наш скорректированный пример](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=961b0db194bbe851ff4d0ed08d3bd98a) сейчас, он больше не работает: ``` error[E0594]: cannot assign to data in a dereference of `std::pin::Pin>` @@ -659,44 +660,44 @@ error[E0596]: cannot borrow data in a dereference of `std::pin::Pin>` ``` -Both errors occur because the `Pin>` type no longer implements the `DerefMut` trait. This is exactly what we wanted because the `DerefMut` trait would return a `&mut` reference, which we wanted to prevent. This only happens because we both opted-out of `Unpin` and changed `Box::new` to `Box::pin`. +Обе ошибки возникают потому, что тип `Pin>` больше не реализует трейт `DerefMut`. Это именно то, чего мы хотели, поскольку трейт `DerefMut` возвращал бы ссылку `&mut`, что мы и хотели предотвратить. Это происходит только потому, что мы отказались от `Unpin` и изменили `Box::new` на `Box::pin`. -The problem now is that the compiler does not only prevent moving the type in line 16, but also forbids initializing the `self_ptr` field in line 10. This happens because the compiler can't differentiate between valid and invalid uses of `&mut` references. To get the initialization working again, we have to use the unsafe [`get_unchecked_mut`] method: +Теперь проблема в том, что компилятор не только предотвращает перемещение типа в строке 16, но и запрещает инициализацию поля `self_ptr` в строке 10. Это происходит потому, что компилятор не может различить допустимые и недопустимые использования ссылок `&mut`. Чтобы инициализация снова заработала, нам нужно использовать небезопасный метод [`get_unchecked_mut`]: [`get_unchecked_mut`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.get_unchecked_mut ```rust -// safe because modifying a field doesn't move the whole struct +// безопасно, т.к. изменение поля не перемещает всю структуру unsafe { let mut_ref = Pin::as_mut(&mut heap_value); Pin::get_unchecked_mut(mut_ref).self_ptr = ptr; } ``` -([Try it on the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b9ebbb11429d9d79b3f9fffe819e2018)) +([Попробовать](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b9ebbb11429d9d79b3f9fffe819e2018)) -The [`get_unchecked_mut`] function works on a `Pin<&mut T>` instead of a `Pin>`, so we have to use [`Pin::as_mut`] for converting the value. Then we can set the `self_ptr` field using the `&mut` reference returned by `get_unchecked_mut`. +Функция [`get_unchecked_mut`] работает с `Pin<&mut T>` вместо `Pin>`, поэтому нам нужно использовать [`Pin::as_mut`] для преобразования значения. Затем мы можем установить поле `self_ptr`, используя ссылку `&mut`, возвращаемую `get_unchecked_mut`. [`Pin::as_mut`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.as_mut -Now the only error left is the desired error on `mem::replace`. Remember, this operation tries to move the heap-allocated value to the stack, which would break the self-reference stored in the `self_ptr` field. By opting out of `Unpin` and using `Pin>`, we can prevent this operation at compile time and thus safely work with self-referential structs. As we saw, the compiler is not able to prove that the creation of the self-reference is safe (yet), so we need to use an unsafe block and verify the correctness ourselves. +Теперь единственной оставшейся ошибкой является желаемая ошибка на `mem::replace`. Помните, что эта операция пытается переместить значение, размещённое в куче, на стек, что нарушило бы самоссылку, хранящуюся в поле `self_ptr`. Отказываясь от `Unpin` и используя `Pin>`, мы можем предотвратить эту операцию на этапе компиляции и таким образом безопасно работать с самоссыльными структурами. Как мы видели, компилятор не может доказать, что создание самоссылки безопасно (пока), поэтому нам нужно использовать небезопасный блок и самостоятельно проверить корректность. -#### Stack Pinning and `Pin<&mut T>` +#### Пиннинг на стеке и `Pin<&mut T>` -In the previous section, we learned how to use `Pin>` to safely create a heap-allocated self-referential value. While this approach works fine and is relatively safe (apart from the unsafe construction), the required heap allocation comes with a performance cost. Since Rust strives to provide _zero-cost abstractions_ whenever possible, the pinning API also allows to create `Pin<&mut T>` instances that point to stack-allocated values. +В предыдущем разделе мы узнали, как использовать `Pin>` для безопасного создания самоссыльного значения, размещённого в куче. Хотя этот подход работает хорошо и относительно безопасен (кроме unsafe), необходимая аллокация в куче бьет по производительности. Поскольку Rust стремится предоставлять _абстракции с нулевыми затратами_ (_zero-cost abstractions_) где это возможно, API закрепления также позволяет создавать экземпляры `Pin<&mut T>`, которые указывают на значения, размещённые на стеке. -Unlike `Pin>` instances, which have _ownership_ of the wrapped value, `Pin<&mut T>` instances only temporarily borrow the wrapped value. This makes things more complicated, as it requires the programmer to ensure additional guarantees themselves. Most importantly, a `Pin<&mut T>` must stay pinned for the whole lifetime of the referenced `T`, which can be difficult to verify for stack-based variables. To help with this, crates like [`pin-utils`] exist, but I still wouldn't recommend pinning to the stack unless you really know what you're doing. +В отличие от экземпляров `Pin>`, которые имеют _владение_ обёрнутым значением, экземпляры `Pin<&mut T>` лишь временно заимствуют обёрнутое значение. Это усложняет задачу, так как программисту необходимо самостоятельно обеспечивать дополнительные гарантии. Важно, чтобы `Pin<&mut T>` оставался закрепленным на протяжении всей жизни ссылочного `T`, что может быть сложно проверить для переменных на стеке. Чтобы помочь с этим, существуют такие крейты, как [`pin-utils`], но я все же не рекомендую закреплять на стеке, если вы не уверены в своих действиях. [`pin-utils`]: https://docs.rs/pin-utils/0.1.0-alpha.4/pin_utils/ -For further reading, check out the documentation of the [`pin` module] and the [`Pin::new_unchecked`] method. +Что бы узнать большое обратитесь к документации модуля [`pin`] и метода [`Pin::new_unchecked`]. [`pin` module]: https://doc.rust-lang.org/nightly/core/pin/index.html [`Pin::new_unchecked`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.new_unchecked -#### Pinning and Futures +#### Пиннинг и Футуры -As we already saw in this post, the [`Future::poll`] method uses pinning in the form of a `Pin<&mut Self>` parameter: +Как мы уже увидели в этом посте, метод [`Future::poll`] использует пиннинг в виде параметра `Pin<&mut Self>`: [`Future::poll`]: https://doc.rust-lang.org/nightly/core/future/trait.Future.html#tymethod.poll @@ -704,43 +705,45 @@ As we already saw in this post, the [`Future::poll`] method uses pinning in the fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll ``` -The reason that this method takes `self: Pin<&mut Self>` instead of the normal `&mut self` is that future instances created from async/await are often self-referential, as we saw [above][self-ref-async-await]. By wrapping `Self` into `Pin` and letting the compiler opt-out of `Unpin` for self-referential futures generated from async/await, it is guaranteed that the futures are not moved in memory between `poll` calls. This ensures that all internal references are still valid. +Причина, по которой этот метод принимает `self: Pin<&mut Self>` вместо обычного `&mut self` в том, что экземпляры футур, созданные через async/await, часто являются самоссыльными, как мы видели [выше][self-ref-async-await]. Оборачивая `Self` в `Pin` и позволяя компилятору отказаться от `Unpin` для самоссыльных футур, генерируемых из async/await, гарантируется, что футуры не будут перемещены в памяти между вызовами `poll`. Это обеспечивает сохранность всех внутренних ссылок. [self-ref-async-await]: @/edition-2/posts/12-async-await/index.md#self-referential-structs -It is worth noting that moving futures before the first `poll` call is fine. This is a result of the fact that futures are lazy and do nothing until they're polled for the first time. The `start` state of the generated state machines therefore only contains the function arguments but no internal references. In order to call `poll`, the caller must wrap the future into `Pin` first, which ensures that the future cannot be moved in memory anymore. Since stack pinning is more difficult to get right, I recommend to always use [`Box::pin`] combined with [`Pin::as_mut`] for this. +Стоит отметить, что перемещение футур до первого вызова `poll` допустимо. Это связано с тем, что футуры являются ленивыми и ничего не делают, пока их не вызовут в первый раз. Состояние `start` сгенерированных конечных автоматов, следовательно, содержит только аргументы функции, но не внутренние ссылки. Чтобы вызвать `poll`, вызывающему необходимо сначала обернуть фьючерс в `Pin`, что гарантирует, что фьючерс больше не может быть перемещён в памяти. Поскольку пиннинг на стеке сложнее сделать правильно, я рекомендую всегда использовать [`Box::pin`] в сочетании с [`Pin::as_mut`] для этого. [`futures`]: https://docs.rs/futures/0.3.4/futures/ -In case you're interested in understanding how to safely implement a future combinator function using stack pinning yourself, take a look at the relatively short [source of the `map` combinator method][map-src] of the `futures` crate and the section about [projections and structural pinning] of the pin documentation. +Если вас интересует, как безопасно реализовать комбинатора футур с использованием закрепления на стеке, взгляните на относительно короткий [исходный код метода комбинатора `map`][map-src] из крейта `futures` и раздел о [projections and structural pinning] в документации pin. [map-src]: https://docs.rs/futures-util/0.3.4/src/futures_util/future/future/map.rs.html [projections and structural pinning]: https://doc.rust-lang.org/stable/std/pin/index.html#projections-and-structural-pinning -### Executors and Wakers +### Executors and Wakers -Using async/await, it is possible to ergonomically work with futures in a completely asynchronous way. However, as we learned above, futures do nothing until they are polled. This means we have to call `poll` on them at some point, otherwise the asynchronous code is never executed. +Используя async/await, можно эргономично работать с футурами в полностью асинхронном режиме. Однако, как мы узнали выше, футуры ничего не делают, пока их не вызовут. Это означает, что нам нужно в какой-то момент вызвать `poll`, иначе асинхронный код никогда не будет выполнен. -With a single future, we can always wait for each future manually using a loop [as described above](#waiting-on-futures). However, this approach is very inefficient and not practical for programs that create a large number of futures. The most common solution to this problem is to define a global _executor_ that is responsible for polling all futures in the system until they are finished. +Запуская одну футуры, мы можем вручную ожидать ее исполнения в цикле, [как описано выше](#waiting-on-futures). Однако этот подход очень неэффективен и непрактичен для программ, создающих большое количество футур. Наиболее распространённым решением этой проблемы является определение глобального _исполнителя_, который отвечает за опрос всех футур в системе, пока они не завершатся. #### Executors -The purpose of an executor is to allow spawning futures as independent tasks, typically through some sort of `spawn` method. The executor is then responsible for polling all futures until they are completed. The big advantage of managing all futures in a central place is that the executor can switch to a different future whenever a future returns `Poll::Pending`. Thus, asynchronous operations are run in parallel and the CPU is kept busy. +Цель исполнителя в том, чтобы позволить создавать футуры в качестве независимых задач, обычно через какой-либо метод `spawn`. Исполнитель затем отвечает за опрос всех футур, пока они не завершатся. Большое преимущество управления всеми футурами в одном месте состоит в том, что исполнитель может переключаться на другую футуру, когда текущая футура возвращает `Poll::Pending`. Таким образом, асинхронные операции выполняются параллельно, и процессор остаётся загруженным. -Many executor implementations can also take advantage of systems with multiple CPU cores. They create a [thread pool] that is able to utilize all cores if there is enough work available and use techniques such as [work stealing] to balance the load between cores. There are also special executor implementations for embedded systems that optimize for low latency and memory overhead. +Многие реализации исполнителей также могут использовать преимущества систем с несколькими ядрами процессора. Они создают [thread pool], способный использовать все ядра, если достаточно работы, и применяют такие техники, как [work stealing], для балансировки нагрузки между ядрами. Существуют также специальные реализации исполнителей для встроенных систем, которые оптимизируют низкую задержку и затраты памяти. [thread pool]: https://en.wikipedia.org/wiki/Thread_pool [work stealing]: https://en.wikipedia.org/wiki/Work_stealing -To avoid the overhead of polling futures repeatedly, executors typically take advantage of the _waker_ API supported by Rust's futures. +Чтобы избежать накладных расходов на повторный опрос футур, исполнители обычно используют API _waker_, поддерживаемый футурами Rust. + + #### Wakers -The idea behind the waker API is that a special [`Waker`] type is passed to each invocation of `poll`, wrapped in the [`Context`] type. This `Waker` type is created by the executor and can be used by the asynchronous task to signal its (partial) completion. As a result, the executor does not need to call `poll` on a future that previously returned `Poll::Pending` until it is notified by the corresponding waker. +Идея API waker в том, что специальный тип [`Waker`] передаётся в каждом вызове `poll`, при этом обернутый в тип [`Context`]. Этот тип `Waker` создаётся исполнителем и может использоваться асинхронной задачей для сигнализации о своём (частичном) завершении. В результате исполнитель не должен вызывать `poll` на футуре, которая ранее вернула `Poll::Pending`, пока не получит уведомление от соответствующего waker. [`Context`]: https://doc.rust-lang.org/nightly/core/task/struct.Context.html -This is best illustrated by a small example: +Лучше всего иллюстрируется небольшим примером: ```rust async fn write_file() { @@ -748,31 +751,31 @@ async fn write_file() { } ``` -This function asynchronously writes the string "Hello" to a `foo.txt` file. Since hard disk writes take some time, the first `poll` call on this future will likely return `Poll::Pending`. However, the hard disk driver will internally store the `Waker` passed to the `poll` call and use it to notify the executor when the file is written to disk. This way, the executor does not need to waste any time trying to `poll` the future again before it receives the waker notification. +Эта функция асинхронно записывает строку "Hello" в файл `foo.txt`. Поскольку запись на жёсткий диск занимает некоторое время, первый вызов `poll` на этой футуре, вероятно, вернёт `Poll::Pending`. Однако драйвер жёсткого диска внутри будет хранить `Waker`, переданный в вызов `poll`, и использовать его для уведомления исполнителя, когда файл будет записан на диск. Таким образом, исполнитель не тратит время на `poll` футуры, пока не получит уведомление от waker. -We will see how the `Waker` type works in detail when we create our own executor with waker support in the implementation section of this post. +Мы увидим, как работает тип `Waker` в деталях, когда создадим свой собственный исполнитель с поддержкой waker в разделе реализации этого поста. ### Cooperative Multitasking? -At the beginning of this post, we talked about preemptive and cooperative multitasking. While preemptive multitasking relies on the operating system to forcibly switch between running tasks, cooperative multitasking requires that the tasks voluntarily give up control of the CPU through a _yield_ operation on a regular basis. The big advantage of the cooperative approach is that tasks can save their state themselves, which results in more efficient context switches and makes it possible to share the same call stack between tasks. +В начале этого поста мы говорили о вытесняющей (preemptive) и кооперативной многозадачности. В то время как вытесняющая многозадачность полагается на операционную систему для принудительного переключения между выполняемыми задачами, кооперативная многозадачность требует, чтобы задачи добровольно уступали контроль над CPU через операцию _yield_ на регулярной основе. Большое преимущество кооперативного подхода в том, что задачи могут сохранять своё состояние самостоятельно, что приводит к более эффективным переключениям контекста и делает возможным совместное использование одного и того же стека вызовов между задачами. -It might not be immediately apparent, but futures and async/await are an implementation of the cooperative multitasking pattern: +Это может не быть сразу очевидным, но футуры и async/await представляют собой реализацию кооперативного паттерна многозадачности: -- Each future that is added to the executor is basically a cooperative task. -- Instead of using an explicit yield operation, futures give up control of the CPU core by returning `Poll::Pending` (or `Poll::Ready` at the end). - - There is nothing that forces futures to give up the CPU. If they want, they can never return from `poll`, e.g., by spinning endlessly in a loop. - - Since each future can block the execution of the other futures in the executor, we need to trust them to not be malicious. -- Futures internally store all the state they need to continue execution on the next `poll` call. With async/await, the compiler automatically detects all variables that are needed and stores them inside the generated state machine. - - Only the minimum state required for continuation is saved. - - Since the `poll` method gives up the call stack when it returns, the same stack can be used for polling other futures. +- Каждая футура, добавляемая в исполнитель, по сути является кооперативной задачей. +- Вместо использования явной операции yield, футуры уступают контроль над ядром CPU, возвращая `Poll::Pending` (или `Poll::Ready` в конце). + - Нет ничего, что заставляло бы футуру уступать CPU. Если они захотят, они могут никогда не возвращаться из `poll`, например, бесконечно выполняя цикл. + - Поскольку каждая футура может блокировать выполнение других футур в исполнителе, нам нужно доверять им, чтобы они не были вредоносными (malicious). +- Футуры внутренне хранят всё состояние, необходимое для продолжения выполнения при следующем вызове `poll`. При использовании async/await компилятор автоматически определяет все переменные, которые необходимы, и сохраняет их внутри сгенерированной машины состояний. + - Сохраняется только минимально необходимое состояние для продолжения. + - Поскольку метод `poll` отдает стек вызовов при возврате, тот же стек может использоваться для опроса других футур. -We see that futures and async/await fit the cooperative multitasking pattern perfectly; they just use some different terminology. In the following, we will therefore use the terms "task" and "future" interchangeably. +Мы видим, что футуры и async/await идеально соответствуют паттерну кооперативной многозадачности; они просто используют другую терминологию. В дальнейшем мы будем использовать термины "задача" и "футура" взаимозаменяемо. ## Implementation -Now that we understand how cooperative multitasking based on futures and async/await works in Rust, it's time to add support for it to our kernel. Since the [`Future`] trait is part of the `core` library and async/await is a feature of the language itself, there is nothing special we need to do to use it in our `#![no_std]` kernel. The only requirement is that we use at least nightly `2020-03-25` of Rust because async/await was not `no_std` compatible before. +Теперь, когда мы понимаем, как работает кооперативная многозадачность на основе футур и async/await в Rust, пора добавить поддержку этого в наш ядро. Поскольку трейт [`Future`] является частью библиотеки `core`, а async/await — это особенность самого языка, нам не нужно делать ничего особенного, чтобы использовать его в нашем `#![no_std]` ядре. Единственное требование — использовать, как минимум, nightly версию Rust от `2020-03-25`, поскольку до этого времени async/await не поддерживала `no_std`. -With a recent-enough nightly, we can start using async/await in our `main.rs`: +С достаточно свежей nightly версией мы можем начать использовать async/await в нашем `main.rs`: ```rust // in src/main.rs @@ -787,13 +790,13 @@ async fn example_task() { } ``` -The `async_number` function is an `async fn`, so the compiler transforms it into a state machine that implements `Future`. Since the function only returns `42`, the resulting future will directly return `Poll::Ready(42)` on the first `poll` call. Like `async_number`, the `example_task` function is also an `async fn`. It awaits the number returned by `async_number` and then prints it using the `println` macro. +Функция `async_number` является `async fn`, поэтому компилятор преобразует её в машину состояний, реализующую `Future`. Поскольку функция возвращает только `42`, результирующая футура непосредственно вернёт `Poll::Ready(42)` при первом вызове `poll`. Как и `async_number`, функция `example_task` также является `async fn`. Она ожидает число, возвращаемое `async_number`, а затем выводит его с помощью макроса `println`. -To run the future returned by `example_task`, we need to call `poll` on it until it signals its completion by returning `Poll::Ready`. To do this, we need to create a simple executor type. +Чтобы запустить футуру, которую вернул `example_task`, нам нужно вызывать `poll` на ней, пока он не сигнализирует о своём завершении, возвращая `Poll::Ready`. Для этого нам нужно создать простой тип исполнителя. ### Task -Before we start the executor implementation, we create a new `task` module with a `Task` type: +Перед тем как начать реализацию исполнителя, мы создаем новый модуль `task` с типом `Task`: ```rust // in src/lib.rs @@ -812,17 +815,17 @@ pub struct Task { } ``` -The `Task` struct is a newtype wrapper around a pinned, heap-allocated, and dynamically dispatched future with the empty type `()` as output. Let's go through it in detail: +Структура `Task` является обёрткой вокруг _закрепленной_, _размещённой в куче_ и _динамически диспетчеризуемой футуры_ с пустым типом `()` в качестве выходного значения. Давайте разберём её подробнее: -- We require that the future associated with a task returns `()`. This means that tasks don't return any result, they are just executed for their side effects. For example, the `example_task` function we defined above has no return value, but it prints something to the screen as a side effect. -- The `dyn` keyword indicates that we store a [_trait object_] in the `Box`. This means that the methods on the future are [_dynamically dispatched_], allowing different types of futures to be stored in the `Task` type. This is important because each `async fn` has its own type and we want to be able to create multiple different tasks. -- As we learned in the [section about pinning], the `Pin` type ensures that a value cannot be moved in memory by placing it on the heap and preventing the creation of `&mut` references to it. This is important because futures generated by async/await might be self-referential, i.e., contain pointers to themselves that would be invalidated when the future is moved. +- Мы требуем, чтобы футура, связанная с задачей, возвращала `()`. Это означает, что задачи не возвращают никаких результатов, они просто выполняются для побочных эффектов. Например, функция `example_task`, которую мы определили выше, не имеет возвращаемого значения, но выводит что-то на экран как побочный эффект (side effect). +- Ключевое слово `dyn` указывает на то, что мы храним [_trait object_] в `Box`. Это означает, что методы на футуре диспетчеризуются динамически, позволяя хранить в типе `Task` разные типы футур. Это важно, поскольку каждая `async fn` имеет свой собственный тип, и мы хотим иметь возможность создавать несколько разных задач. +- Как мы узнали в [разделе о закреплении], тип `Pin` обеспечивает, что значение не может быть перемещено в памяти, помещая его в кучу и предотвращая создание `&mut` ссылок на него. Это важно, потому что фьючерсы, генерируемые async/await, могут быть самоссыльными, т.е. содержать указатели на себя, которые станут недействительными, если футура будет перемещена. [_trait object_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html [_dynamically dispatched_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html#trait-objects-perform-dynamic-dispatch -[section about pinning]: #pinning +[разделе о закреплении]: #pinning -To allow the creation of new `Task` structs from futures, we create a `new` function: +Чтобы разрешить создание новых структур `Task` из фьючерсов, мы создаём функцию `new`: ```rust // in src/task/mod.rs @@ -836,9 +839,10 @@ impl Task { } ``` -The function takes an arbitrary future with an output type of `()` and pins it in memory through the [`Box::pin`] function. Then it wraps the boxed future in the `Task` struct and returns it. The `'static` lifetime is required here because the returned `Task` can live for an arbitrary time, so the future needs to be valid for that time too. +Функция принимает произвольную футуру с выходным типом `()` и закрепляет его в памяти через [`Box::pin`]. Затем она оборачивает упакованную футуру в структуру `Task` и возвращает ее. Здесь нужно время жизни`'static`, т.к. возвращаемый `Task` может жить произвольное время, следовательно, футура также должна быть действительнтой в течение этого времени. + +Мы также добавляем метод `poll`, чтобы позволить исполнителю опрашивать хранимую футуру: -We also add a `poll` method to allow the executor to poll the stored future: ```rust // in src/task/mod.rs @@ -852,11 +856,12 @@ impl Task { } ``` -Since the [`poll`] method of the `Future` trait expects to be called on a `Pin<&mut T>` type, we use the [`Pin::as_mut`] method to convert the `self.future` field of type `Pin>` first. Then we call `poll` on the converted `self.future` field and return the result. Since the `Task::poll` method should only be called by the executor that we'll create in a moment, we keep the function private to the `task` module. +Поскольку метод [`poll`] трейта `Future` ожидает вызова на типе `Pin<&mut T>`, мы сначала используем метод [`Pin::as_mut`], чтобы преобразовать поле `self.future` типа `Pin>`. Затем мы вызываем `poll` на преобразованном поле `self.future` и возвращаем результат. Поскольку метод `Task::poll` должен вызываться только исполнителем, который мы создадим через мгновение, мы оставляем функцию приватной для модуля `task`. + ### Simple Executor -Since executors can be quite complex, we deliberately start by creating a very basic executor before implementing a more featureful executor later. For this, we first create a new `task::simple_executor` submodule: +Поскольку исполнители могут быть довольно сложными, мы намеренно начинаем с создания очень базового исполнителя, прежде чем реализовывать более продвинутого. Для этого мы сначала создаём новый подмодуль `task::simple_executor`: ```rust // in src/task/mod.rs @@ -887,14 +892,14 @@ impl SimpleExecutor { } ``` -The struct contains a single `task_queue` field of type [`VecDeque`], which is basically a vector that allows for push and pop operations on both ends. The idea behind using this type is that we insert new tasks through the `spawn` method at the end and pop the next task for execution from the front. This way, we get a simple [FIFO queue] (_"first in, first out"_). +Структура содержит единственное поле `task_queue` типа [`VecDeque`], которое по сути является вектором, позволяющим выполнять операции добавления и удаления с обоих концов. Идея в том, что мы можем вставлять новые задачи через метод `spawn` в конец и извлекаем следующую задачу для выполнения из начала. Таким образом, мы получаем простую [FIFO очередь] ("первый пришёл — первый вышел"). [`VecDeque`]: https://doc.rust-lang.org/stable/alloc/collections/vec_deque/struct.VecDeque.html -[FIFO queue]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics) +[FIFO очередь]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics) #### Dummy Waker -In order to call the `poll` method, we need to create a [`Context`] type, which wraps a [`Waker`] type. To start simple, we will first create a dummy waker that does nothing. For this, we create a [`RawWaker`] instance, which defines the implementation of the different `Waker` methods, and then use the [`Waker::from_raw`] function to turn it into a `Waker`: +Чтобы вызвать метод `poll`, нам нужно создать тип [`Context`], который оборачивает тип [`Waker`]. Начнём с простого: мы сначала создадим заглушку waker, которая ничего не делает. Для этого мы создаём экземпляр [`RawWaker`], который определяет реализацию различных методов `Waker`, а затем используем функцию [`Waker::from_raw`], чтобы превратить его в `Waker`: [`RawWaker`]: https://doc.rust-lang.org/stable/core/task/struct.RawWaker.html [`Waker::from_raw`]: https://doc.rust-lang.org/stable/core/task/struct.Waker.html#method.from_raw @@ -913,17 +918,18 @@ fn dummy_waker() -> Waker { } ``` -The `from_raw` function is unsafe because undefined behavior can occur if the programmer does not uphold the documented requirements of `RawWaker`. Before we look at the implementation of the `dummy_raw_waker` function, we first try to understand how the `RawWaker` type works. +Функция `from_raw` является небезопасной, может быть неопределенное поведение (undefined behavior), если программист не соблюдает документированные требования к `RawWaker`. Прежде чем мы рассмотрим реализацию функции `dummy_raw_waker`, давайте сначала попытаемся понять, как работает тип `RawWaker`. + ##### `RawWaker` -The [`RawWaker`] type requires the programmer to explicitly define a [_virtual method table_] (_vtable_) that specifies the functions that should be called when the `RawWaker` is cloned, woken, or dropped. The layout of this vtable is defined by the [`RawWakerVTable`] type. Each function receives a `*const ()` argument, which is a _type-erased_ pointer to some value. The reason for using a `*const ()` pointer instead of a proper reference is that the `RawWaker` type should be non-generic but still support arbitrary types. The pointer is provided by putting it into the `data` argument of [`RawWaker::new`], which just initializes a `RawWaker`. The `Waker` then uses this `RawWaker` to call the vtable functions with `data`. +Тип [`RawWaker`] требует от программиста явного определения [_таблицы виртуальных методов_] (_vtable_), которая указывает функции, которые должны быть вызваны при клонировании (cloned), пробуждении (woken) или удалении (droppen) `RawWaker`. Расположение этой vtable определяется типом [`RawWakerVTable`]. Каждая функция получает аргумент `*const ()`, который является _type-erased_ указателем на некоторое значение. Причина использования указателя `*const ()` вместо правильной ссылки в том, что тип `RawWaker` должен быть non-generic, но при этом поддерживать произвольные типы. Указатель передается в аргументе `data` ф-ции [`RawWaker::new`], которая просто инициализирует `RawWaker`. Затем `Waker` использует этот `RawWaker`, чтобы вызывать функции vtable с `data`. -[_virtual method table_]: https://en.wikipedia.org/wiki/Virtual_method_table +[_таблицы виртуальных методов_]: https://en.wikipedia.org/wiki/Virtual_method_table [`RawWakerVTable`]: https://doc.rust-lang.org/stable/core/task/struct.RawWakerVTable.html [`RawWaker::new`]: https://doc.rust-lang.org/stable/core/task/struct.RawWaker.html#method.new -Typically, the `RawWaker` is created for some heap-allocated struct that is wrapped into the [`Box`] or [`Arc`] type. For such types, methods like [`Box::into_raw`] can be used to convert the `Box` to a `*const T` pointer. This pointer can then be cast to an anonymous `*const ()` pointer and passed to `RawWaker::new`. Since each vtable function receives the same `*const ()` as an argument, the functions can safely cast the pointer back to a `Box` or a `&T` to operate on it. As you can imagine, this process is highly dangerous and can easily lead to undefined behavior on mistakes. For this reason, manually creating a `RawWaker` is not recommended unless necessary. +Как правило, `RawWaker` создаётся для какой-то структуры, размещённой в куче, которая обёрнута в тип [`Box`] или [`Arc`]. Для таких типов можно использовать методы, такие как [`Box::into_raw`], чтобы преобразовать `Box` в указатель `*const T`. Этот указатель затем можно привести к анонимному указателю `*const ()` и передать в `RawWaker::new`. Поскольку каждая функция vtable получает один и тот же `*const ()` в качестве аргумента, функции могут безопасно привести указатель обратно к `Box` или `&T`, чтобы работать с ним. Как вы можете себе представить, этот процесс крайне опасен и легко может привести к неопределённому поведению в случае ошибок. По этой причине вручную создавать `RawWaker` не рекомендуется, если это не является необходимым. [`Box`]: https://doc.rust-lang.org/stable/alloc/boxed/struct.Box.html [`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html @@ -931,7 +937,7 @@ Typically, the `RawWaker` is created for some heap-allocated struct that is wrap ##### A Dummy `RawWaker` -While manually creating a `RawWaker` is not recommended, there is currently no other way to create a dummy `Waker` that does nothing. Fortunately, the fact that we want to do nothing makes it relatively safe to implement the `dummy_raw_waker` function: +Хотя вручную создавать `RawWaker` не рекомендуется, в настоящее время нет другого способа создать заглушку `Waker`, которая ничего не делает. К счастью, тот факт, что мы хотим ничего не делать, делает реализацию функции `dummy_raw_waker` относительно безопасной: ```rust // in src/task/simple_executor.rs @@ -949,13 +955,13 @@ fn dummy_raw_waker() -> RawWaker { } ``` -First, we define two inner functions named `no_op` and `clone`. The `no_op` function takes a `*const ()` pointer and does nothing. The `clone` function also takes a `*const ()` pointer and returns a new `RawWaker` by calling `dummy_raw_waker` again. We use these two functions to create a minimal `RawWakerVTable`: The `clone` function is used for the cloning operations, and the `no_op` function is used for all other operations. Since the `RawWaker` does nothing, it does not matter that we return a new `RawWaker` from `clone` instead of cloning it. +Сначала мы определяем две внутренние функции с именами `no_op` и `clone`. Функция `no_op` принимает указатель `*const ()` и ничего не делает. Функция `clone` также принимает указатель `*const ()` и возвращает новый `RawWaker`, снова вызывая `dummy_raw_waker`. Мы используем эти две функции для создания минимальной `RawWakerVTable`: функция `clone` используется для операций клонирования, а функция `no_op` — для всех остальных операций. Поскольку `RawWaker` ничего не делает, не имеет значения, что мы возвращаем новый `RawWaker` из `clone` вместо его клонирования. -After creating the `vtable`, we use the [`RawWaker::new`] function to create the `RawWaker`. The passed `*const ()` does not matter since none of the vtable functions use it. For this reason, we simply pass a null pointer. +После создания `vtable` мы используем функцию [`RawWaker::new`] для создания `RawWaker`. Переданный `*const ()` не имеет значения, поскольку ни одна из функций vtable не использует его. По этой причине мы просто передаем нулевой указатель. #### A `run` Method -Now we have a way to create a `Waker` instance, we can use it to implement a `run` method on our executor. The most simple `run` method is to repeatedly poll all queued tasks in a loop until all are done. This is not very efficient since it does not utilize the notifications of the `Waker` type, but it is an easy way to get things running: +Теперь у нас есть способ создать экземпляр `Waker`, и мы можем использовать его для реализации метода `run` в нашем исполнителе. Самый простой метод `run` — это многократный опрос всех задач в очереди в цикле до тех пор, пока все они не будут выполнены. Это не очень эффективно, так как не использует уведомления от `Waker`, но это простой способ запустить эти штуки: ```rust // in src/task/simple_executor.rs @@ -968,7 +974,7 @@ impl SimpleExecutor { let waker = dummy_waker(); let mut context = Context::from_waker(&waker); match task.poll(&mut context) { - Poll::Ready(()) => {} // task done + Poll::Ready(()) => {} // task готов Poll::Pending => self.task_queue.push_back(task), } } @@ -976,11 +982,11 @@ impl SimpleExecutor { } ``` -The function uses a `while let` loop to handle all tasks in the `task_queue`. For each task, it first creates a `Context` type by wrapping a `Waker` instance returned by our `dummy_waker` function. Then it invokes the `Task::poll` method with this `context`. If the `poll` method returns `Poll::Ready`, the task is finished and we can continue with the next task. If the task is still `Poll::Pending`, we add it to the back of the queue again so that it will be polled again in a subsequent loop iteration. +Функция использует цикл `while let`, чтобы обработать все задачи в `task_queue`. Для каждой задачи сначала создаётся тип `Context`, оборачивая экземпляр `Waker`, возвращаемый нашей функцией `dummy_waker`. Затем вызывается метод `Task::poll` с этим `context`. Если метод `poll` возвращает `Poll::Ready`, задача завершена, и мы можем продолжить с следующей задачей. Если задача всё ещё `Poll::Pending`, мы добавляем её в конец очереди, чтобы она была опрошена снова в следующей итерации цикла. #### Trying It -With our `SimpleExecutor` type, we can now try running the task returned by the `example_task` function in our `main.rs`: +С нашим типом `SimpleExecutor` мы теперь можем попробовать запустить задачу, возвращаемую функцией `example_task`, в нашем `main.rs`: ```rust // in src/main.rs @@ -988,17 +994,17 @@ With our `SimpleExecutor` type, we can now try running the task returned by the use blog_os::task::{Task, simple_executor::SimpleExecutor}; fn kernel_main(boot_info: &'static BootInfo) -> ! { - // […] initialization routines, including `init_heap` + // […] инициализация всякого, включая `init_heap` let mut executor = SimpleExecutor::new(); executor.spawn(Task::new(example_task())); executor.run(); - // […] test_main, "it did not crash" message, hlt_loop + // […] test_main, "It did not crash!" сообщение, hlt_loop } -// Below is the example_task function again so that you don't have to scroll up +// ниже example_task, что бы вам не нужно было скролить async fn async_number() -> u32 { 42 @@ -1010,21 +1016,21 @@ async fn example_task() { } ``` -When we run it, we see that the expected _"async number: 42"_ message is printed to the screen: +Когда мы запускаем её, мы видим, что ожидаемое сообщение _"async number: 42"_ выводится на экран: -![QEMU printing "Hello World", "async number: 42", and "It did not crash!"](qemu-simple-executor.png) +![QEMU печатает "Hello World", "async number: 42" и "It did not crash!"](qemu-simple-executor.png) -Let's summarize the various steps that happen in this example: +Давайте подытожим шаги, которые происходят в этом примере: -- First, a new instance of our `SimpleExecutor` type is created with an empty `task_queue`. -- Next, we call the asynchronous `example_task` function, which returns a future. We wrap this future in the `Task` type, which moves it to the heap and pins it, and then add the task to the `task_queue` of the executor through the `spawn` method. -- We then call the `run` method to start the execution of the single task in the queue. This involves: - - Popping the task from the front of the `task_queue`. - - Creating a `RawWaker` for the task, converting it to a [`Waker`] instance, and then creating a [`Context`] instance from it. - - Calling the [`poll`] method on the future of the task, using the `Context` we just created. - - Since the `example_task` does not wait for anything, it can directly run till its end on the first `poll` call. This is where the _"async number: 42"_ line is printed. - - Since the `example_task` directly returns `Poll::Ready`, it is not added back to the task queue. -- The `run` method returns after the `task_queue` becomes empty. The execution of our `kernel_main` function continues and the _"It did not crash!"_ message is printed. +- Сначала создаётся новый экземпляр нашего типа `SimpleExecutor` с пустой `task_queue`. +- Затем мы вызываем асинхронную функцию `example_task`, которая возвращает футуру. Мы оборачиваем эту футуру в тип `Task`, который перемещает её в кучу и закрепляет, а затем добавляем задачу в `task_queue` исполнителя через метод `spawn`. +- После этого мы вызываем метод `run`, чтобы начать выполнение единственной задачи в очереди. Это включает в себя: + - Извлечение задачи из начала `task_queue`. + - Создание `RawWaker` для задачи, преобразование его в экземпляр [`Waker`] и создание экземпляра [`Context`] на его основе. + - Вызов метода [`poll`] на футуре задачи, используя только что созданный `Context`. + - Поскольку `example_task` не ждёт ничего, она может непосредственно выполняться до конца при первом вызове `poll`. Именно здесь выводится строка _"async number: 42"_. + - Т.к `example_task` напрямую возвращает `Poll::Ready`, она не добавляется обратно в очередь задач. +- Метод `run` возвращается после того, как `task_queue` становится пустым. Выполнение нашей функции `kernel_main` продолжается, и выводится сообщение _"It did not crash!"_. ### Async Keyboard Input From 4c4d721a58a0fa8d5b2c45eaa94746d14caf0cdc Mon Sep 17 00:00:00 2001 From: TakiMoysha <36836047+TakiMoysha@users.noreply.github.com> Date: Sat, 13 Sep 2025 03:56:30 +0200 Subject: [PATCH 4/9] translated the whole text --- .../posts/12-async-await/index.ru.md | 314 +++++++++--------- 1 file changed, 157 insertions(+), 157 deletions(-) diff --git a/blog/content/edition-2/posts/12-async-await/index.ru.md b/blog/content/edition-2/posts/12-async-await/index.ru.md index 25c51742..547b14c8 100644 --- a/blog/content/edition-2/posts/12-async-await/index.ru.md +++ b/blog/content/edition-2/posts/12-async-await/index.ru.md @@ -1034,36 +1034,36 @@ async fn example_task() { ### Async Keyboard Input -Our simple executor does not utilize the `Waker` notifications and simply loops over all tasks until they are done. This wasn't a problem for our example since our `example_task` can directly run to finish on the first `poll` call. To see the performance advantages of a proper `Waker` implementation, we first need to create a task that is truly asynchronous, i.e., a task that will probably return `Poll::Pending` on the first `poll` call. +Наш простой исполнитель не использует уведомления `Waker` и просто циклически обрабатывает все задачи до тех пор, пока они не завершатся. Это не было проблемой для нашего примера, так как наш `example_task` может завершиться сразу при первом вызове `poll`. Чтобы увидеть преимущества производительности правильной реализации `Waker`, нам нужно сначала создать задачу, которая действительно асинхронна, т.е. задачу, которая, вероятно, вернёт `Poll::Pending` при первом вызове `poll`. -We already have some kind of asynchronicity in our system that we can use for this: hardware interrupts. As we learned in the [_Interrupts_] post, hardware interrupts can occur at arbitrary points in time, determined by some external device. For example, a hardware timer sends an interrupt to the CPU after some predefined time has elapsed. When the CPU receives an interrupt, it immediately transfers control to the corresponding handler function defined in the interrupt descriptor table (IDT). +У нас уже есть некий вид асинхронности в нашей системе, который мы можем использовать для этого: аппаратные прерывания. Как мы узнали в посте [_Interrupts_], аппаратные прерывания могут происходить в произвольные моменты времени, определяемые каким-либо внешним устройством. Например, аппаратный таймер отправляет прерывание процессору после истечения заданного времени. Когда процессор получает прерывание, он немедленно передаёт управление соответствующей функции-обработчику, определённой в таблице дескрипторов прерываний (IDT). [_Interrupts_]: @/edition-2/posts/07-hardware-interrupts/index.md -In the following, we will create an asynchronous task based on the keyboard interrupt. The keyboard interrupt is a good candidate for this because it is both non-deterministic and latency-critical. Non-deterministic means that there is no way to predict when the next key press will occur because it is entirely dependent on the user. Latency-critical means that we want to handle the keyboard input in a timely manner, otherwise the user will feel a lag. To support such a task in an efficient way, it will be essential that the executor has proper support for `Waker` notifications. +В дальнейшем мы создадим асинхронную задачу на основе прерывания клавиатуры. Прерывание клавиатуры выбраны т.к. это хороший кандидат, т.к. это они недетерминированны, так и критично по времени задержки. Недетерминированность означает, что невозможно предсказать, когда произойдёт нажатие клавиши, поскольку это полностью зависит от пользователя. Критичность по времени задержки означает, что мы хотим обрабатывать ввод с клавиатуры своевременно, иначе пользователь почувствует задержку. Чтобы эффективно поддерживать такую задачу, исполнителю будет необходимо обеспечить надлежащую поддержку уведомлений `Waker`. #### Scancode Queue -Currently, we handle the keyboard input directly in the interrupt handler. This is not a good idea for the long term because interrupt handlers should stay as short as possible as they might interrupt important work. Instead, interrupt handlers should only perform the minimal amount of work necessary (e.g., reading the keyboard scancode) and leave the rest of the work (e.g., interpreting the scancode) to a background task. +Сейчас мы обрабатываем ввод с клавиатуры непосредственно в обработчике прерываний. Это нехорошая реализация в долгосрочной перспективе, потому что обработчики прерываний должны быть как можно короче ( ), так как они могут прерывать важную работу. Вместо этого обработчики прерываний должны выполнять только минимальный объем необходимой работы (например, считывание кода сканирования клавиатуры) и оставлять остальную работу (например, интерпретацию кода сканирования) фоновой задаче. -A common pattern for delegating work to a background task is to create some sort of queue. The interrupt handler pushes units of work to the queue, and the background task handles the work in the queue. Applied to our keyboard interrupt, this means that the interrupt handler only reads the scancode from the keyboard, pushes it to the queue, and then returns. The keyboard task sits on the other end of the queue and interprets and handles each scancode that is pushed to it: +Распространённым шаблоном для делегирования работы фоновым задачам является очередь. Обработчик прерываний добавляет единицы работы в очередь, а фоновая задача обрабатывает работу в очереди. Применительно к нашему прерыванию клавиатуры это означает, что обработчик прерываний только считывает скан-код с клавиатуры, добавляет его в очередь, а затем возвращается. Задача клавиатуры находится на другом конце очереди и интерпретирует и обрабатывает каждый скан-код, который в неё добавляется: -![Scancode queue with 8 slots on the top. Keyboard interrupt handler on the bottom left with a "push scancode" arrow to the left of the queue. Keyboard task on the bottom right with a "pop scancode" arrow coming from the right side of the queue.](scancode-queue.svg) +![Очередь скан-кодов с 8 слотами вверху. Обработчик прерываний клавиатуры внизу слева с стрелкой "добавить скан-код" слева от очереди. Задача клавиатуры внизу справа со стрелкой "извлечь скан-код", идущей с правой стороны очереди.](scancode-queue.svg) -A simple implementation of that queue could be a mutex-protected [`VecDeque`]. However, using mutexes in interrupt handlers is not a good idea since it can easily lead to deadlocks. For example, when the user presses a key while the keyboard task has locked the queue, the interrupt handler tries to acquire the lock again and hangs indefinitely. Another problem with this approach is that `VecDeque` automatically increases its capacity by performing a new heap allocation when it becomes full. This can lead to deadlocks again because our allocator also uses a mutex internally. Further problems are that heap allocations can fail or take a considerable amount of time when the heap is fragmented. +Простая реализация такой очереди может быть основана на `VecDeque`, защищённом мьютексом. Однако использование мьютексов в обработчиках прерываний — не очень хорошая идея, так как это может легко привести к взаимным блокировкам (deadlock). Например, пользователь нажимает клавишу, но в тот же момент задача клавиатуру заблокировала очередь, обработчик прерываний пытается снова захватить блокировку и застревает навсегда. Ещё одна проблема с этим подходом в том, что `VecDeque` автоматически увеличивает свою ёмкость, выполняя новое выделение памяти в куче, когда она заполняется. Это также может привести к взаимным блокировкам, так как наш аллокатор также использует внутренний мьютекс. Другими проблемами являются то, что выделение памяти в куче может не удаться или занять значительное время, когда куча фрагментирована. -To prevent these problems, we need a queue implementation that does not require mutexes or allocations for its `push` operation. Such queues can be implemented by using lock-free [atomic operations] for pushing and popping elements. This way, it is possible to create `push` and `pop` operations that only require a `&self` reference and are thus usable without a mutex. To avoid allocations on `push`, the queue can be backed by a pre-allocated fixed-size buffer. While this makes the queue _bounded_ (i.e., it has a maximum length), it is often possible to define reasonable upper bounds for the queue length in practice, so that this isn't a big problem. +Чтобы предотвратить эти проблемы, нам нужна реализация очереди, которая не требует мьютексов или выделений памяти для своей операции `push`. Такие очереди могут быть реализованы с использованием неблокирующих [атомарных операций] для добавления и извлечения элементов. Таким образом, возможно создать операции `push` и `pop`, которые требуют только ссылки `&self` и могут использоваться без мьютекса. Чтобы избежать выделений памяти при `push`, очередь может быть основана на заранее выделенном буфере фиксированного размера. Хотя это делает очередь _ограниченной_ (_bounded_) (т.е. у неё есть максимальная длина), на практике часто возможно определить разумные верхние границы для длины очереди, так что это не представляет собой большой проблемы. [atomic operations]: https://doc.rust-lang.org/core/sync/atomic/index.html ##### The `crossbeam` Crate -Implementing such a queue in a correct and efficient way is very difficult, so I recommend sticking to existing, well-tested implementations. One popular Rust project that implements various mutex-free types for concurrent programming is [`crossbeam`]. It provides a type named [`ArrayQueue`] that is exactly what we need in this case. And we're lucky: the type is fully compatible with `no_std` crates with allocation support. +Реализовать такую очередь правильно и эффективно очень сложно, поэтому я рекомендую придерживаться существующих, хорошо протестированных реализаций. Один из популярных проектов на Rust, который реализует различные типы без мьютексов для конкурентного программирования — это [`crossbeam`]. Он предоставляет тип под названием [`ArrayQueue`], который именно то, что нам нужно в данном случае. И нам повезло: этот тип полностью совместим с `no_std` библиотеками, поддерживающими выделение памяти. [`crossbeam`]: https://github.com/crossbeam-rs/crossbeam [`ArrayQueue`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html -To use the type, we need to add a dependency on the `crossbeam-queue` crate: +Чтобы использовать этот тип, нам нужно добавить зависимость на библиотеку `crossbeam-queue`: ```toml # in Cargo.toml @@ -1074,20 +1074,20 @@ default-features = false features = ["alloc"] ``` -By default, the crate depends on the standard library. To make it `no_std` compatible, we need to disable its default features and instead enable the `alloc` feature. (Note that we could also add a dependency on the main `crossbeam` crate, which re-exports the `crossbeam-queue` crate, but this would result in a larger number of dependencies and longer compile times.) +По умолчанию библиотека зависит от стандартной библиотеки. Чтобы сделать её совместимой с `no_std`, нам нужно отключить её стандартные функции и вместо этого включить функцию `alloc`. (Заметьте, что мы также могли бы добавить зависимость на основную библиотеку `crossbeam`, которая повторно экспортирует библиотеку `crossbeam-queue`, но это привело бы к большему количеству зависимостей и более длительному времени компиляции.) ##### Queue Implementation -Using the `ArrayQueue` type, we can now create a global scancode queue in a new `task::keyboard` module: +Используя тип `ArrayQueue`, мы теперь можем создать глобальную очередь скан-кодов в новом модуле `task::keyboard`: ```rust -// in src/task/mod.rs +// src/task/mod.rs pub mod keyboard; ``` ```rust -// in src/task/keyboard.rs +// src/task/keyboard.rs use conquer_once::spin::OnceCell; use crossbeam_queue::ArrayQueue; @@ -1095,7 +1095,7 @@ use crossbeam_queue::ArrayQueue; static SCANCODE_QUEUE: OnceCell> = OnceCell::uninit(); ``` -Since [`ArrayQueue::new`] performs a heap allocation, which is not possible at compile time ([yet][const-heap-alloc]), we can't initialize the static variable directly. Instead, we use the [`OnceCell`] type of the [`conquer_once`] crate, which makes it possible to perform a safe one-time initialization of static values. To include the crate, we need to add it as a dependency in our `Cargo.toml`: +Поскольку [`ArrayQueue::new`] выполняет выделение памяти в куче, что невозможно на этапе компиляции ([пока что][const-heap-alloc]), мы не можем инициализировать статическую переменную напрямую. Вместо этого мы используем тип [`OnceCell`] из библиотеки [`conquer_once`], который позволяет безопасно выполнить одноразовую инициализацию статических значений. Чтобы включить библиотеку, нам нужно добавить её как зависимость в наш `Cargo.toml`: [`ArrayQueue::new`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.new [const-heap-alloc]: https://github.com/rust-lang/const-eval/issues/20 @@ -1103,29 +1103,29 @@ Since [`ArrayQueue::new`] performs a heap allocation, which is not possible at c [`conquer_once`]: https://docs.rs/conquer-once/0.2.0/conquer_once/index.html ```toml -# in Cargo.toml +# Cargo.toml [dependencies.conquer-once] version = "0.2.0" default-features = false ``` -Instead of the [`OnceCell`] primitive, we could also use the [`lazy_static`] macro here. However, the `OnceCell` type has the advantage that we can ensure that the initialization does not happen in the interrupt handler, thus preventing the interrupt handler from performing a heap allocation. +Вместо примитива [`OnceCell`] мы также могли бы использовать макрос [`lazy_static`]. Однако тип `OnceCell` имеет то преимущество, что мы можем гарантировать, что инициализация не произойдёт в обработчике прерываний, тем самым предотвращая выполнение выделения памяти в куче в обработчике прерываний. [`lazy_static`]: https://docs.rs/lazy_static/1.4.0/lazy_static/index.html #### Filling the Queue -To fill the scancode queue, we create a new `add_scancode` function that we will call from the interrupt handler: +Чтобы заполнить очередь скан-кодов, мы создаём новую функцию `add_scancode`, которую будем вызывать из обработчика прерываний: ```rust -// in src/task/keyboard.rs +// src/task/keyboard.rs use crate::println; -/// Called by the keyboard interrupt handler +/// вызывается обработчиком прерываний клавиатуры /// -/// Must not block or allocate. +/// не должен блокировать или аллоцировать. pub(crate) fn add_scancode(scancode: u8) { if let Ok(queue) = SCANCODE_QUEUE.try_get() { if let Err(_) = queue.push(scancode) { @@ -1137,18 +1137,18 @@ pub(crate) fn add_scancode(scancode: u8) { } ``` -We use [`OnceCell::try_get`] to get a reference to the initialized queue. If the queue is not initialized yet, we ignore the keyboard scancode and print a warning. It's important that we don't try to initialize the queue in this function because it will be called by the interrupt handler, which should not perform heap allocations. Since this function should not be callable from our `main.rs`, we use the `pub(crate)` visibility to make it only available to our `lib.rs`. +Мы используем [`OnceCell::try_get`] для получения ссылки на инициализированную очередь. Если очередь ещё не инициализирована, мы игнорируем скан-код клавиатуры и выводим предупреждение. Важно, чтобы мы не пытались инициализировать очередь в этой функции, так как она будет вызываться обработчиком прерываний, который не должен выполнять выделения памяти в куче. Поскольку эта функция не должна быть доступна из нашего `main.rs`, мы используем видимость `pub(crate)`, чтобы сделать её доступной только для нашего `lib.rs`. [`OnceCell::try_get`]: https://docs.rs/conquer-once/0.2.0/conquer_once/raw/struct.OnceCell.html#method.try_get -The fact that the [`ArrayQueue::push`] method requires only a `&self` reference makes it very simple to call the method on the static queue. The `ArrayQueue` type performs all the necessary synchronization itself, so we don't need a mutex wrapper here. In case the queue is full, we print a warning too. +Тот факт, что метод [`ArrayQueue::push`] требует только ссылки `&self`, делает его очень простым для вызова на статической очереди. Тип `ArrayQueue` выполняет все необходимые синхронизации сам, поэтому нам не нужен мьютекс-обёртка. В случае, если очередь полна, мы также выводим предупреждение. [`ArrayQueue::push`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.push -To call the `add_scancode` function on keyboard interrupts, we update our `keyboard_interrupt_handler` function in the `interrupts` module: +Чтобы вызывать функцию `add_scancode` при прерываниях клавиатуры, мы обновляем нашу функцию `keyboard_interrupt_handler` в модуле `interrupts`: ```rust -// in src/interrupts.rs +// src/interrupts.rs extern "x86-interrupt" fn keyboard_interrupt_handler( _stack_frame: InterruptStackFrame @@ -1157,7 +1157,7 @@ extern "x86-interrupt" fn keyboard_interrupt_handler( let mut port = Port::new(0x60); let scancode: u8 = unsafe { port.read() }; - crate::task::keyboard::add_scancode(scancode); // new + crate::task::keyboard::add_scancode(scancode); // новое unsafe { PICS.lock() @@ -1166,16 +1166,16 @@ extern "x86-interrupt" fn keyboard_interrupt_handler( } ``` -We removed all the keyboard handling code from this function and instead added a call to the `add_scancode` function. The rest of the function stays the same as before. +Мы убрали весь код обработки клавиатуры из этой функции и вместо этого добавили вызов функции `add_scancode`. Остальная часть функции остаётся такой же, как и прежде. -As expected, keypresses are no longer printed to the screen when we run our project using `cargo run` now. Instead, we see the warning that the scancode queue is uninitialized for every keystroke. +Как и ожидалось, нажатия клавиш больше не выводятся на экран, когда мы запускаем наш проект с помощью `cargo run`. Вместо этого пишется предупреждение, что очередь не инициализирована при каждом нажатия клавиши. #### Scancode Stream -To initialize the `SCANCODE_QUEUE` and read the scancodes from the queue in an asynchronous way, we create a new `ScancodeStream` type: +Чтобы инициализировать `SCANCODE_QUEUE` и считывать скан-коды из очереди асинхронным способом, мы создаём новый тип `ScancodeStream`: ```rust -// in src/task/keyboard.rs +// src/task/keyboard.rs pub struct ScancodeStream { _private: (), @@ -1190,13 +1190,13 @@ impl ScancodeStream { } ``` -The purpose of the `_private` field is to prevent construction of the struct from outside of the module. This makes the `new` function the only way to construct the type. In the function, we first try to initialize the `SCANCODE_QUEUE` static. We panic if it is already initialized to ensure that only a single `ScancodeStream` instance can be created. +Цель поля `_private` — предотвратить создание структуры из внешних модулей. Это делает функцию `new` единственным способом создать данный тип. В функции мы сначала пытаемся инициализировать статическую переменную `SCANCODE_QUEUE`. Если она уже инициализирована, мы вызываем панику, чтобы гарантировать, что можно создать только один экземпляр `ScancodeStream`. -To make the scancodes available to asynchronous tasks, the next step is to implement a `poll`-like method that tries to pop the next scancode off the queue. While this sounds like we should implement the [`Future`] trait for our type, this does not quite fit here. The problem is that the `Future` trait only abstracts over a single asynchronous value and expects that the `poll` method is not called again after it returns `Poll::Ready`. Our scancode queue, however, contains multiple asynchronous values, so it is okay to keep polling it. +Чтобы сделать скан-коды доступными для асинхронных задач, далее нужно реализовать метод, подобный `poll`, который пытается извлечь следующий скан-код из очереди. Хотя это звучит так, будто мы должны реализовать трейт [`Future`] для нашего типа, здесь он не подходит. Проблема в том, что трейт `Future` абстрагируется только над одним асинхронным значением и ожидает, что метод `poll` не будет вызываться снова после того, как он вернёт `Poll::Ready`. Наша очередь скан-кодов, однако, содержит несколько асинхронных значений, поэтому нормально продолжать опрашивать её. ##### The `Stream` Trait -Since types that yield multiple asynchronous values are common, the [`futures`] crate provides a useful abstraction for such types: the [`Stream`] trait. The trait is defined like this: +Поскольку типы, которые возвращают несколько асинхронных значений, являются распространёнными, библиотека [`futures`] предоставляет полезную абстракцию для таких типов: трейт [`Stream`]. Трейт определяется следующим образом: [`Stream`]: https://rust-lang.github.io/async-book/05_streams/01_chapter.html @@ -1209,21 +1209,21 @@ pub trait Stream { } ``` -This definition is quite similar to the [`Future`] trait, with the following differences: +Это определение довольно похоже на трейт [`Future`], с следующими отличиями: -- The associated type is named `Item` instead of `Output`. -- Instead of a `poll` method that returns `Poll`, the `Stream` trait defines a `poll_next` method that returns a `Poll>` (note the additional `Option`). +- Ассоциированный тип называется `Item`, а не `Output`. +- Вместо метода `poll`, который возвращает `Poll`, трейт `Stream` определяет метод `poll_next`, который возвращает `Poll>` (обратите внимание на дополнительный `Option`). -There is also a semantic difference: The `poll_next` can be called repeatedly, until it returns `Poll::Ready(None)` to signal that the stream is finished. In this regard, the method is similar to the [`Iterator::next`] method, which also returns `None` after the last value. +Существует также семантическое отличие: метод `poll_next` можно вызывать многократно, пока он не вернёт `Poll::Ready(None)`, чтобы сигнализировать о том, что поток завершён. В этом отношении метод похож на метод [`Iterator::next`], который также возвращает `None` после последнего значения. [`Iterator::next`]: https://doc.rust-lang.org/stable/core/iter/trait.Iterator.html#tymethod.next ##### Implementing `Stream` -Let's implement the `Stream` trait for our `ScancodeStream` to provide the values of the `SCANCODE_QUEUE` in an asynchronous way. For this, we first need to add a dependency on the `futures-util` crate, which contains the `Stream` type: +Давайте реализуем трейт `Stream` для нашего `ScancodeStream`, чтобы предоставлять значения из `SCANCODE_QUEUE` асинхронным способом. Для этого нам сначала нужно добавить зависимость на библиотеку `futures-util`, которая содержит тип `Stream`: ```toml -# in Cargo.toml +# Cargo.toml [dependencies.futures-util] version = "0.3.4" @@ -1231,12 +1231,12 @@ default-features = false features = ["alloc"] ``` -We disable the default features to make the crate `no_std` compatible and enable the `alloc` feature to make its allocation-based types available (we will need this later). (Note that we could also add a dependency on the main `futures` crate, which re-exports the `futures-util` crate, but this would result in a larger number of dependencies and longer compile times.) +Мы отключаем стандартные функции, чтобы сделать библиотеку совместимой с `no_std`, и включаем функцию `alloc`, чтобы сделать доступными её типы, основанные на выделении памяти (это понадобится позже). (Заметьте, что мы также могли бы добавить зависимость на основную библиотеку `futures`, которая повторно экспортирует библиотеку `futures-util`, но это привело бы к большему количеству зависимостей и более длительному времени компиляции.) -Now we can import and implement the `Stream` trait: +Теперь мы можем импортировать и реализовать трейт `Stream`: ```rust -// in src/task/keyboard.rs +// src/task/keyboard.rs use core::{pin::Pin, task::{Poll, Context}}; use futures_util::stream::Stream; @@ -1254,40 +1254,40 @@ impl Stream for ScancodeStream { } ``` -We first use the [`OnceCell::try_get`] method to get a reference to the initialized scancode queue. This should never fail since we initialize the queue in the `new` function, so we can safely use the `expect` method to panic if it's not initialized. Next, we use the [`ArrayQueue::pop`] method to try to get the next element from the queue. If it succeeds, we return the scancode wrapped in `Poll::Ready(Some(…))`. If it fails, it means that the queue is empty. In that case, we return `Poll::Pending`. +Сначала мы используем метод [`OnceCell::try_get`] для получения ссылки на инициализированную очередь скан-кодов. Это никогда не должно вызывать ошибок, так как мы инициализируем очередь в функции `new`, поэтому мы можем безопасно использовать метод `expect`. Далее мы используем метод [`ArrayQueue::pop`] для попытки получить следующий элемент из очереди. Если это успешно, мы возвращаем скан-код, обёрнутый в `Poll::Ready(Some(…))`. Если это не удаётся, это означает, что очередь пуста. В этом случае мы возвращаем `Poll::Pending`. [`ArrayQueue::pop`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.pop #### Waker Support -Like the `Futures::poll` method, the `Stream::poll_next` method requires the asynchronous task to notify the executor when it becomes ready after `Poll::Pending` is returned. This way, the executor does not need to poll the same task again until it is notified, which greatly reduces the performance overhead of waiting tasks. +Как и метод `Futures::poll`, метод `Stream::poll_next` требует от асинхронной задачи уведомить исполнителя, когда она становится готовой после возврата `Poll::Pending`. Таким образом, исполнителю не нужно повторно опрашивать ту же задачу, пока она не будет уведомлена, что значительно снижает накладные расходы на ожидание задач. -To send this notification, the task should extract the [`Waker`] from the passed [`Context`] reference and store it somewhere. When the task becomes ready, it should invoke the [`wake`] method on the stored `Waker` to notify the executor that the task should be polled again. +Чтобы отправить это уведомление, задача должна извлечь [`Waker`] из переданной ссылки [`Context`] и сохранить его где-то. Когда задача становится готовой, она должна вызвать метод [`wake`] на сохранённом `Waker`, чтобы уведомить исполнителя о том, что задачу следует опросить снова. ##### AtomicWaker -To implement the `Waker` notification for our `ScancodeStream`, we need a place where we can store the `Waker` between poll calls. We can't store it as a field in the `ScancodeStream` itself because it needs to be accessible from the `add_scancode` function. The solution to this is to use a static variable of the [`AtomicWaker`] type provided by the `futures-util` crate. Like the `ArrayQueue` type, this type is based on atomic instructions and can be safely stored in a `static` and modified concurrently. +Чтобы реализовать уведомление `Waker` для нашего `ScancodeStream`, нам нужно место, где мы можем хранить `Waker` между вызовами `poll`. Мы не можем хранить его как поле в самом `ScancodeStream`, потому что он должен быть доступен из функции `add_scancode`. Решение этой проблемы — использование статической переменной типа [`AtomicWaker`], предоставляемой библиотекой `futures-util`. Как и тип `ArrayQueue`, этот тип основан на атомарных инструкциях и может безопасно храниться в `static` и модифицироваться параллельно. [`AtomicWaker`]: https://docs.rs/futures-util/0.3.4/futures_util/task/struct.AtomicWaker.html -Let's use the [`AtomicWaker`] type to define a static `WAKER`: +Давайте используем тип [`AtomicWaker`] для определения статической переменной `WAKER`: ```rust -// in src/task/keyboard.rs +// src/task/keyboard.rs use futures_util::task::AtomicWaker; static WAKER: AtomicWaker = AtomicWaker::new(); ``` -The idea is that the `poll_next` implementation stores the current waker in this static, and the `add_scancode` function calls the `wake` function on it when a new scancode is added to the queue. +Идея в том, что реализация `poll_next` хранит текущий `waker` в этой статической переменной, а функция `add_scancode` вызывает функцию `wake` на ней, когда новый скан-код добавляется в очередь. ##### Storing a Waker -The contract defined by `poll`/`poll_next` requires the task to register a wakeup for the passed `Waker` when it returns `Poll::Pending`. Let's modify our `poll_next` implementation to satisfy this requirement: +Контракт, определяемый `poll`/`poll_next`, требует, чтобы задача зарегистрировала уведомление для переданного `Waker`, когда она возвращает `Poll::Pending`. Давайте изменим нашу реализацию `poll_next`, чтобы удовлетворить этому требованию: ```rust -// in src/task/keyboard.rs +// src/task/keyboard.rs impl Stream for ScancodeStream { type Item = u8; @@ -1297,7 +1297,7 @@ impl Stream for ScancodeStream { .try_get() .expect("scancode queue not initialized"); - // fast path + // первый путь if let Some(scancode) = queue.pop() { return Poll::Ready(Some(scancode)); } @@ -1314,30 +1314,30 @@ impl Stream for ScancodeStream { } ``` -Like before, we first use the [`OnceCell::try_get`] function to get a reference to the initialized scancode queue. We then optimistically try to `pop` from the queue and return `Poll::Ready` when it succeeds. This way, we can avoid the performance overhead of registering a waker when the queue is not empty. +Как и прежде, сначала мы используем функцию [`OnceCell::try_get`] для получения ссылки на инициализированную очередь скан-кодов. Затем мы оптимистично пытаемся выполнить `pop` из очереди и возвращаем `Poll::Ready`, при успехе. Таким образом, мы можем избежать накладных расходов на регистрацию `waker`, когда очередь не пуста. -If the first call to `queue.pop()` does not succeed, the queue is potentially empty. Only potentially because the interrupt handler might have filled the queue asynchronously immediately after the check. Since this race condition can occur again for the next check, we need to register the `Waker` in the `WAKER` static before the second check. This way, a wakeup might happen before we return `Poll::Pending`, but it is guaranteed that we get a wakeup for any scancodes pushed after the check. +Если первый вызов `queue.pop()` неуспешен, то очередь потенциально пуста. Потенциально, потому что обработчик прерываний мог заполнить очередь асинхронно сразу после проверки. Поскольку это состояние гонки может возникнуть снова для следующей проверки, мы должны зарегистрировать `Waker` в статической переменной `WAKER` перед второй проверкой. Таким образом, уведомление может произойти до того, как мы вернём `Poll::Pending`, но гарантируется, что мы получим уведомление для любых скан-кодов, добавленных после проверки. -After registering the `Waker` contained in the passed [`Context`] through the [`AtomicWaker::register`] function, we try to pop from the queue a second time. If it now succeeds, we return `Poll::Ready`. We also remove the registered waker again using [`AtomicWaker::take`] because a waker notification is no longer needed. In case `queue.pop()` fails for a second time, we return `Poll::Pending` like before, but this time with a registered wakeup. +После регистрации `Waker`, содержащегося в переданном [`Context`], через функцию [`AtomicWaker::register`], мы пытаемся выполнить `pop` из очереди во второй раз. Если теперь это получается, мы возвращаем `Poll::Ready`. Мы также снова удаляем зарегистрированный `waker` с помощью [`AtomicWaker::take`], т.к. уведомление `waker` больше не нужно. Если `queue.pop()` снова неуспешно, мы возвращаем `Poll::Pending`, как и прежде, но на этот раз с зарегистрированным уведомлением. [`AtomicWaker::register`]: https://docs.rs/futures-util/0.3.4/futures_util/task/struct.AtomicWaker.html#method.register [`AtomicWaker::take`]: https://docs.rs/futures/0.3.4/futures/task/struct.AtomicWaker.html#method.take -Note that there are two ways that a wakeup can happen for a task that did not return `Poll::Pending` (yet). One way is the mentioned race condition when the wakeup happens immediately before returning `Poll::Pending`. The other way is when the queue is no longer empty after registering the waker, so that `Poll::Ready` is returned. Since these spurious wakeups are not preventable, the executor needs to be able to handle them correctly. +Обратите внимание, что существует два способа, которыми может произойти уведомление для задачи, которая ещё не вернула `Poll::Pending`. Один из способов — это упомянутое состояние гонки, когда уведомление происходит незадолго до возвращения `Poll::Pending`. Другой способ — это когда очередь больше не пуста после регистрации `waker`, так что возвращается `Poll::Ready`. Поскольку эти ложные уведомления предотвратить невозможно, исполнитель должен уметь правильно с ними справляться. ##### Waking the Stored Waker -To wake the stored `Waker`, we add a call to `WAKER.wake()` in the `add_scancode` function: +Чтобы разбудить сохранённый `Waker`, мы добавляем вызов `WAKER.wake()` в функцию `add_scancode`: ```rust -// in src/task/keyboard.rs +// src/task/keyboard.rs pub(crate) fn add_scancode(scancode: u8) { if let Ok(queue) = SCANCODE_QUEUE.try_get() { if let Err(_) = queue.push(scancode) { println!("WARNING: scancode queue full; dropping keyboard input"); } else { - WAKER.wake(); // new + WAKER.wake(); // новое } } else { println!("WARNING: scancode queue uninitialized"); @@ -1345,18 +1345,18 @@ pub(crate) fn add_scancode(scancode: u8) { } ``` -The only change that we made is to add a call to `WAKER.wake()` if the push to the scancode queue succeeds. If a waker is registered in the `WAKER` static, this method will call the equally-named [`wake`] method on it, which notifies the executor. Otherwise, the operation is a no-op, i.e., nothing happens. +Единственное изменение, которое мы внесли, — это добавление вызова `WAKER.wake()`, если добавление в очередь скан-кодов прошло успешно. Если в статической переменной `WAKER` зарегистрирован `waker`, этот метод вызовет одноимённый метод [`wake`] на нём, который уведомляет исполнителя. В противном случае операция не приводит к никаким действиям (no-op). [`wake`]: https://doc.rust-lang.org/stable/core/task/struct.Waker.html#method.wake -It is important that we call `wake` only after pushing to the queue because otherwise the task might be woken too early while the queue is still empty. This can, for example, happen when using a multi-threaded executor that starts the woken task concurrently on a different CPU core. While we don't have thread support yet, we will add it soon and don't want things to break then. +Важно, чтобы мы вызывали `wake` только после добавления в очередь, потому что в противном случае задача может быть разбужена слишком рано, пока очередь всё ещё пуста. Это может, например, произойти при использовании многопоточного исполнителя, который запускает пробуждённую задачу параллельно на другом ядре CPU. Хотя у нас пока нет поддержки потоков, мы добавим её скоро и не хотим, чтобы всё сломалось в этом случае. #### Keyboard Task -Now that we implemented the `Stream` trait for our `ScancodeStream`, we can use it to create an asynchronous keyboard task: +Теперь, когда мы реализовали трейт `Stream` для нашего `ScancodeStream`, мы можем использовать его для создания асинхронной задачи клавиатуры: ```rust -// in src/task/keyboard.rs +// src/task/keyboard.rs use futures_util::stream::StreamExt; use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1}; @@ -1380,60 +1380,60 @@ pub async fn print_keypresses() { } ``` -The code is very similar to the code we had in our [keyboard interrupt handler] before we modified it in this post. The only difference is that, instead of reading the scancode from an I/O port, we take it from the `ScancodeStream`. For this, we first create a new `Scancode` stream and then repeatedly use the [`next`] method provided by the [`StreamExt`] trait to get a `Future` that resolves to the next element in the stream. By using the `await` operator on it, we asynchronously wait for the result of the future. +Код очень похож на тот, который у нас был в нашем обработчике прерываний клавиатуры ([keyboard interrupt handler]) до того, как мы его изменили в этом посте. Единственное различие в том, что вместо чтения скан-кода из порта ввода-вывода мы берем его из `ScancodeStream`. Для этого мы сначала создаем новый поток `Scancode`, а затем многократно используем метод [`next`], предоставляемый трейтами [`StreamExt`], чтобы получить `Future`, который разрешается в следующий элемент потока. Используя оператор `await`, мы асинхронно ожидаем результат этого `Future`. [keyboard interrupt handler]: @/edition-2/posts/07-hardware-interrupts/index.md#interpreting-the-scancodes [`next`]: https://docs.rs/futures-util/0.3.4/futures_util/stream/trait.StreamExt.html#method.next [`StreamExt`]: https://docs.rs/futures-util/0.3.4/futures_util/stream/trait.StreamExt.html -We use `while let` to loop until the stream returns `None` to signal its end. Since our `poll_next` method never returns `None`, this is effectively an endless loop, so the `print_keypresses` task never finishes. +Мы используем `while let`, чтобы проходить цикл, пока поток не вернет `None`, сигнализируя о своем завершении. Поскольку наш метод `poll_next` никогда не возвращает `None`, это фактически бесконечный цикл, поэтому задача `print_keypresses` никогда не завершается. -Let's add the `print_keypresses` task to our executor in our `main.rs` to get working keyboard input again: +Давайте добавим задачу `print_keypresses` в наш исполнитель в `main.rs`, чтобы снова получить работающий ввод с клавиатуры: ```rust -// in src/main.rs +// src/main.rs use blog_os::task::keyboard; // new fn kernel_main(boot_info: &'static BootInfo) -> ! { - // […] initialization routines, including init_heap, test_main + // […] инициализация всякого, включая init_heap, test_main let mut executor = SimpleExecutor::new(); executor.spawn(Task::new(example_task())); - executor.spawn(Task::new(keyboard::print_keypresses())); // new + executor.spawn(Task::new(keyboard::print_keypresses())); // новое executor.run(); - // […] "it did not crash" message, hlt_loop + // […] сообщение "it did not crash", hlt_loop } ``` -When we execute `cargo run` now, we see that keyboard input works again: +Когда мы выполняем `cargo run` сейчас, мы видим, что ввод с клавиатуры снова работает: -![QEMU printing ".....H...e...l...l..o..... ...W..o..r....l...d...!"](qemu-keyboard-output.gif) +![QEMU печатает ".....H...e...l...l..o..... ...W..o..r....l...d...!"](qemu-keyboard-output.gif) -If you keep an eye on the CPU utilization of your computer, you will see that the `QEMU` process now continuously keeps the CPU busy. This happens because our `SimpleExecutor` polls tasks over and over again in a loop. So even if we don't press any keys on the keyboard, the executor repeatedly calls `poll` on our `print_keypresses` task, even though the task cannot make any progress and will return `Poll::Pending` each time. +Если вы будете следить за загрузкой процессора вашего компьютера, вы увидите, что процесс `QEMU` теперь постоянно загружает CPU. Это происходит потому, что наш `SimpleExecutor` многократно опрашивает задачи в цикле. Поэтому даже если мы не нажимаем никаких клавиш на клавиатуре, исполнитель снова и снова вызывает `poll` для нашей задачи `print_keypresses`, хотя задача не может добиться прогресса и будет каждый раз возвращать `Poll::Pending`. ### Executor with Waker Support -To fix the performance problem, we need to create an executor that properly utilizes the `Waker` notifications. This way, the executor is notified when the next keyboard interrupt occurs, so it does not need to keep polling the `print_keypresses` task over and over again. +Чтобы решить проблему производительности, нам нужно создать исполнитель, который правильно использует уведомления `Waker`. Таким образом, исполнитель будет уведомлен, когда произойдет следующее прерывание клавиатуры, и ему не нужно будет постоянно опрашивать задачу `print_keypresses`. #### Task Id -The first step in creating an executor with proper support for waker notifications is to give each task a unique ID. This is required because we need a way to specify which task should be woken. We start by creating a new `TaskId` wrapper type: +Первый шаг в создании исполнителя с правильной поддержкой уведомлений waker — это дать каждой задаче уникальный идентификатор. Это необходимо, потому что нам нужно иметь способ указать, какую задачу следует разбудить. Мы начинаем с создания нового типа-обёртки `TaskId`: ```rust -// in src/task/mod.rs +// src/task/mod.rs #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] struct TaskId(u64); ``` -The `TaskId` struct is a simple wrapper type around `u64`. We derive a number of traits for it to make it printable, copyable, comparable, and sortable. The latter is important because we want to use `TaskId` as the key type of a [`BTreeMap`] in a moment. +Структура `TaskId` — это простая обёртка вокруг `u64`. Мы добавляем `derive` для того, что бы она была печатаемой, сравнимой, копируемой и сортируемой. Последнее важно, т.к. в дальнейшем мы хотим использовать `TaskId` в качестве типа ключа для [`BTreeMap`]. [`BTreeMap`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html -To create a new unique ID, we create a `TaskId::new` function: +Для создания нового уникального идентификатора мы создаём функцию `TaskId::new`: ```rust use core::sync::atomic::{AtomicU64, Ordering}; @@ -1446,46 +1446,46 @@ impl TaskId { } ``` -The function uses a static `NEXT_ID` variable of type [`AtomicU64`] to ensure that each ID is assigned only once. The [`fetch_add`] method atomically increases the value and returns the previous value in one atomic operation. This means that even when the `TaskId::new` method is called in parallel, every ID is returned exactly once. The [`Ordering`] parameter defines whether the compiler is allowed to reorder the `fetch_add` operation in the instructions stream. Since we only require that the ID be unique, the `Relaxed` ordering with the weakest requirements is enough in this case. +Функция использует статическую переменную `NEXT_ID` типа [`AtomicU64`], чтобы гарантировать, что каждый идентификатор присваивается только один раз. Метод [`fetch_add`] атомарно увеличивает значение и возвращает предыдущее за одну атомарную операцию. Это значит, что даже когда метод `TaskId::new` вызывается параллельно, каждый идентификатор возвращается ровно один раз. Параметр [`Ordering`] определяет, может ли компилятор переупорядочить операцию `fetch_add` в потоке инструкций. Поскольку мы только требуем, чтобы идентификатор был уникальным, в этом случае достаточно использования упорядочивание `Relaxed` с самыми слабыми требованиями. [`AtomicU64`]: https://doc.rust-lang.org/core/sync/atomic/struct.AtomicU64.html [`fetch_add`]: https://doc.rust-lang.org/core/sync/atomic/struct.AtomicU64.html#method.fetch_add [`Ordering`]: https://doc.rust-lang.org/core/sync/atomic/enum.Ordering.html -We can now extend our `Task` type with an additional `id` field: +Теперь мы можем расширить наш тип `Task`, добавив поле `id`: ```rust -// in src/task/mod.rs +// src/task/mod.rs pub struct Task { - id: TaskId, // new + id: TaskId, // новое future: Pin>>, } impl Task { pub fn new(future: impl Future + 'static) -> Task { Task { - id: TaskId::new(), // new + id: TaskId::new(), // новое future: Box::pin(future), } } } ``` -The new `id` field makes it possible to uniquely name a task, which is required for waking a specific task. +Новое поле `id` позволяет уникально называть задачу, что необходимо для пробуждения конкретной задачи. #### The `Executor` Type -We create our new `Executor` type in a `task::executor` module: +Мы создаем наш новый тип `Executor` в модуле `task::executor`: ```rust -// in src/task/mod.rs +// src/task/mod.rs pub mod executor; ``` ```rust -// in src/task/executor.rs +// src/task/executor.rs use super::{Task, TaskId}; use alloc::{collections::BTreeMap, sync::Arc}; @@ -1509,25 +1509,25 @@ impl Executor { } ``` -Instead of storing tasks in a [`VecDeque`] like we did for our `SimpleExecutor`, we use a `task_queue` of task IDs and a [`BTreeMap`] named `tasks` that contains the actual `Task` instances. The map is indexed by the `TaskId` to allow efficient continuation of a specific task. +Вместо хранения задач в [`VecDeque`], как мы делали для нашего `SimpleExecutor`, мы используем `task_queue` с идентификаторами задач и [`BTreeMap`] с именем `tasks`, который содержит фактические экземпляры `Task`. Карта индексируется по `TaskId`, что позволяет эффективно продолжать выполнение конкретной задачи. -The `task_queue` field is an [`ArrayQueue`] of task IDs, wrapped into the [`Arc`] type that implements _reference counting_. Reference counting makes it possible to share ownership of the value among multiple owners. It works by allocating the value on the heap and counting the number of active references to it. When the number of active references reaches zero, the value is no longer needed and can be deallocated. +Поле `task_queue` представляет собой [`ArrayQueue`] идентификаторов задач, обёрнутую в тип [`Arc`], который реализует _счётчик ссылок_ (_reference counting_). Счётчик ссылок позволяет разделять владение значением между несколькими владельцами. Он создает место в куче и записывает туда кол-во активных ссылок. Когда количество активных ссылок достигает нуля, значение больше не нужно и может быть освобождено. -We use this `Arc` type for the `task_queue` because it will be shared between the executor and wakers. The idea is that the wakers push the ID of the woken task to the queue. The executor sits on the receiving end of the queue, retrieves the woken tasks by their ID from the `tasks` map, and then runs them. The reason for using a fixed-size queue instead of an unbounded queue such as [`SegQueue`] is that interrupt handlers should not allocate on push to this queue. +Мы используем тип `Arc` для `task_queue`, потому что он будет разделяться между исполнителем и wakers. Идея в том, что wakers добавляют идентификатор разбуженной задачи в очередь. Исполнитель находится на приемной стороне очереди, извлекает разбуженные задачи по их идентификатору из `tasks` дерева и затем выполняет их. Причина использования фиксированной очереди вместо неограниченной, такой как [`SegQueue`] в том, что обработчики прерываний не должны выделять память при добавлении в эту очередь. -In addition to the `task_queue` and the `tasks` map, the `Executor` type has a `waker_cache` field that is also a map. This map caches the [`Waker`] of a task after its creation. This has two reasons: First, it improves performance by reusing the same waker for multiple wake-ups of the same task instead of creating a new waker each time. Second, it ensures that reference-counted wakers are not deallocated inside interrupt handlers because it could lead to deadlocks (there are more details on this below). +В дополнение к `task_queue` и дереве `tasks`, тип `Executor` имеет поле `waker_cache`, которое также является деревом. Это дерево кэширует [`Waker`] задачи после его создания. На это имеется две причины: во-первых, это улучшает производительность, повторно используя тот же waker для нескольких пробуждений одной и той же задачи, вместо создания нового waker каждый раз. Во-вторых, это гарантирует, что wakers с подсчётом ссылок не освобождаются внутри обработчиков прерываний, поскольку это может привести к взаимным блокировкам (подробнее об ниже). [`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html [`SegQueue`]: https://docs.rs/crossbeam-queue/0.2.1/crossbeam_queue/struct.SegQueue.html -To create an `Executor`, we provide a simple `new` function. We choose a capacity of 100 for the `task_queue`, which should be more than enough for the foreseeable future. In case our system will have more than 100 concurrent tasks at some point, we can easily increase this size. +Чтобы создать `Executor`, мы предоставляем простую функцию `new`. Мы выбираем ёмкость 100 для `task_queue`, что должно быть более чем достаточно на обозримое будущее. В случае, если в нашей системе в какой-то момент будет больше 100 параллельных задач, мы можем легко увеличить этот размер. #### Spawning Tasks -As for the `SimpleExecutor`, we provide a `spawn` method on our `Executor` type that adds a given task to the `tasks` map and immediately wakes it by pushing its ID to the `task_queue`: +Как и в `SimpleExecutor`, мы предоставляем метод `spawn` для нашего типа `Executor`, который добавляет данную задачу в дерево `tasks` и немедленно пробуждает её, добавляя её идентификатор в `task_queue`: ```rust -// in src/task/executor.rs +// src/task/executor.rs impl Executor { pub fn spawn(&mut self, task: Task) { @@ -1540,20 +1540,20 @@ impl Executor { } ``` -If there is already a task with the same ID in the map, the [`BTreeMap::insert`] method returns it. This should never happen since each task has a unique ID, so we panic in this case since it indicates a bug in our code. Similarly, we panic when the `task_queue` is full since this should never happen if we choose a large-enough queue size. +Если в карте уже существует задача с тем же идентификатором, метод [`BTreeMap::insert`] возвращает её. Это никогда не должно происходить, поскольку каждая задача имеет уникальный идентификатор, поэтому в этом случае мы вызываем панику, так как это указывает на ошибку в нашем коде. Аналогично, мы вызываем панику, когда `task_queue` полна, так как этого никогда не должно происходить, если мы выбираем достаточно большой размер очереди. #### Running Tasks -To execute all tasks in the `task_queue`, we create a private `run_ready_tasks` method: +Чтобы выполнить все задачи в `task_queue`, мы создаём приватный метод `run_ready_tasks`: ```rust -// in src/task/executor.rs +// src/task/executor.rs use core::task::{Context, Poll}; impl Executor { fn run_ready_tasks(&mut self) { - // destructure `self` to avoid borrow checker errors + // деструктуризация `self` что бы избежать ошибок проверки заимствования (borrow checker) let Self { tasks, task_queue, @@ -1563,7 +1563,7 @@ impl Executor { while let Some(task_id) = task_queue.pop() { let task = match tasks.get_mut(&task_id) { Some(task) => task, - None => continue, // task no longer exists + None => continue, // task больше нету }; let waker = waker_cache .entry(task_id) @@ -1571,7 +1571,7 @@ impl Executor { let mut context = Context::from_waker(waker); match task.poll(&mut context) { Poll::Ready(()) => { - // task done -> remove it and its cached waker + // задача готова -> удалить ее и кеширумый waker tasks.remove(&task_id); waker_cache.remove(&task_id); } @@ -1582,15 +1582,15 @@ impl Executor { } ``` -The basic idea of this function is similar to our `SimpleExecutor`: Loop over all tasks in the `task_queue`, create a waker for each task, and then poll them. However, instead of adding pending tasks back to the end of the `task_queue`, we let our `TaskWaker` implementation take care of adding woken tasks back to the queue. The implementation of this waker type will be shown in a moment. +Смысл функции схож со смыслом `SimpleExecutor`: циклично перебираем все задачи в `task_queue`, создаём waker для каждой задачи и затем опрашиваем их. Однако вместо того, чтобы добавлять ожидающие задачи обратно в конец `task_queue`, мы позволяем реализации нашего `TaskWaker` заботиться о добавлении пробуждённых задач обратно в очередь. Реализация этого типа waker будет показана через мгновение. -Let's look into some of the implementation details of this `run_ready_tasks` method: +Давайте рассмотрим некоторые детали реализации этого метода `run_ready_tasks`: -- We use [_destructuring_] to split `self` into its three fields to avoid some borrow checker errors. Namely, our implementation needs to access the `self.task_queue` from within a closure, which currently tries to borrow `self` completely. This is a fundamental borrow checker issue that will be resolved when [RFC 2229] is [implemented][RFC 2229 impl]. +- Мы используем _деструктуризацию_ [_destructuring_], чтобы разделить `self` на три поля, чтобы избежать некоторых ошибок компилятора. В частности, наша реализация требует доступа к `self.task_queue` изнутри замыкания, что в данный момент пытается полностью заимствовать `self`. Это фундаментальная проблема компилятора, которая будет решена в [RFC 2229], [проблема][RFC 2229 impl]. -- For each popped task ID, we retrieve a mutable reference to the corresponding task from the `tasks` map. Since our `ScancodeStream` implementation registers wakers before checking whether a task needs to be put to sleep, it might happen that a wake-up occurs for a task that no longer exists. In this case, we simply ignore the wake-up and continue with the next ID from the queue. +- Для каждого извлеченного идентификатора задачи мы получаем мутабельную ссылку на соответствующую задачу из дерева `tasks`. Поскольку наша реализация `ScancodeStream` регистрирует wakers перед проверкой, нужно ли задачу отправить в сон, может случиться так, что произойдёт пробуждение для задачи, которой больше не существует. В этом случае мы просто игнорируем пробуждение и продолжаем с следующим идентификатором из очереди. -- To avoid the performance overhead of creating a waker on each poll, we use the `waker_cache` map to store the waker for each task after it has been created. For this, we use the [`BTreeMap::entry`] method in combination with [`Entry::or_insert_with`] to create a new waker if it doesn't exist yet and then get a mutable reference to it. For creating a new waker, we clone the `task_queue` and pass it together with the task ID to the `TaskWaker::new` function (implementation shown below). Since the `task_queue` is wrapped into an `Arc`, the `clone` only increases the reference count of the value, but still points to the same heap-allocated queue. Note that reusing wakers like this is not possible for all waker implementations, but our `TaskWaker` type will allow it. +- Чтобы избежать накладных расходов на создание waker при каждом опросе, мы используем дерево `waker_cache` для хранения waker для каждой задачи после ее создания. Для этого мы используем метод [`BTreeMap::entry`] в сочетании с [`Entry::or_insert_with`] для создания нового waker, если он ещё не существует, а затем получаем на него мутабельную ссылку. Для создания нового waker мы клонируем `task_queue` и передаём его вместе с идентификатором задачи в функцию `TaskWaker::new` (реализация ниже). Поскольку `task_queue` обёрнута в `Arc`, `clone` только увеличивает счётчик ссылок на значение, но всё равно указывает на ту же выделенную в куче очередь. Обратите внимание, что повторное использование wakers таким образом невозможно для всех реализаций waker, но наш тип `TaskWaker` это позволит. [_destructuring_]: https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#destructuring-to-break-apart-values [RFC 2229]: https://github.com/rust-lang/rfcs/pull/2229 @@ -1599,16 +1599,16 @@ Let's look into some of the implementation details of this `run_ready_tasks` met [`BTreeMap::entry`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html#method.entry [`Entry::or_insert_with`]: https://doc.rust-lang.org/alloc/collections/btree_map/enum.Entry.html#method.or_insert_with -A task is finished when it returns `Poll::Ready`. In that case, we remove it from the `tasks` map using the [`BTreeMap::remove`] method. We also remove its cached waker, if it exists. +Задача считается завершённой, когда она возвращает `Poll::Ready`. В этом случае мы удаляем её из дерева `tasks` с помощью метода [`BTreeMap::remove`]. Мы также удаляем её кэшированный waker, если он существует. [`BTreeMap::remove`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html#method.remove #### Waker Design -The job of the waker is to push the ID of the woken task to the `task_queue` of the executor. We implement this by creating a new `TaskWaker` struct that stores the task ID and a reference to the `task_queue`: +Задача waker — добавить идентификатор разбуженной задачи в `task_queue` исполнителя. Мы реализуем это, создавая новую структуру `TaskWaker`, которая хранит идентификатор задачи и ссылку на `task_queue`: ```rust -// in src/task/executor.rs +// src/task/executor.rs struct TaskWaker { task_id: TaskId, @@ -1616,14 +1616,14 @@ struct TaskWaker { } ``` -Since the ownership of the `task_queue` is shared between the executor and wakers, we use the [`Arc`] wrapper type to implement shared reference-counted ownership. +Поскольку владение `task_queue` разделяется между исполнителем и wakers, мы используем обёртку типа [`Arc`] для реализации совместного владения с подсчётом ссылок. [`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html -The implementation of the wake operation is quite simple: +Реализация операции пробуждения довольно проста: ```rust -// in src/task/executor.rs +// src/task/executor.rs impl TaskWaker { fn wake_task(&self) { @@ -1632,18 +1632,18 @@ impl TaskWaker { } ``` -We push the `task_id` to the referenced `task_queue`. Since modifications to the [`ArrayQueue`] type only require a shared reference, we can implement this method on `&self` instead of `&mut self`. +Мы добавляем `task_id` в ссылку на `task_queue`. Поскольку модификации типа [`ArrayQueue`] требуют только совместной ссылки, мы можем реализовать этот метод на `&self`, а не на `&mut self`. ##### The `Wake` Trait -In order to use our `TaskWaker` type for polling futures, we need to convert it to a [`Waker`] instance first. This is required because the [`Future::poll`] method takes a [`Context`] instance as an argument, which can only be constructed from the `Waker` type. While we could do this by providing an implementation of the [`RawWaker`] type, it's both simpler and safer to instead implement the `Arc`-based [`Wake`][wake-trait] trait and then use the [`From`] implementations provided by the standard library to construct the `Waker`. +Чтобы использовать наш тип `TaskWaker` для опроса futures, нам нужно сначала преобразовать его в экземпляр [`Waker`]. Это необходимо, потому что метод [`Future::poll`] принимает экземпляр [`Context`] в качестве аргумента, который можно создать только из типа `Waker`. Хотя мы могли бы сделать это, предоставив реализацию типа [`RawWaker`], проще и безопаснее реализовать трейт [`Wake`][wake-trait] на основе `Arc` и затем использовать реализации [`From`], предоставленные стандартной библиотекой, для создания `Waker`. -The trait implementation looks like this: +Реализация трейта выглядит следующим образом: [wake-trait]: https://doc.rust-lang.org/nightly/alloc/task/trait.Wake.html ```rust -// in src/task/executor.rs +// src/task/executor.rs use alloc::task::Wake; @@ -1658,18 +1658,18 @@ impl Wake for TaskWaker { } ``` -Since wakers are commonly shared between the executor and the asynchronous tasks, the trait methods require that the `Self` instance is wrapped in the [`Arc`] type, which implements reference-counted ownership. This means that we have to move our `TaskWaker` to an `Arc` in order to call them. +Поскольку wakers обычно разделяются между исполнителем и асинхронными задачами, методы трейта требуют, чтобы экземпляр `Self` был обёрнут в тип [`Arc`], который реализует владение с подсчётом ссылок. Это означает, что нам нужно переместить наш `TaskWaker` в `Arc`, чтобы вызвать их. -The difference between the `wake` and `wake_by_ref` methods is that the latter only requires a reference to the `Arc`, while the former takes ownership of the `Arc` and thus often requires an increase of the reference count. Not all types support waking by reference, so implementing the `wake_by_ref` method is optional. However, it can lead to better performance because it avoids unnecessary reference count modifications. In our case, we can simply forward both trait methods to our `wake_task` function, which requires only a shared `&self` reference. +Разница между методами `wake` и `wake_by_ref` заключается в том, что последний требует только ссылки на `Arc`, в то время как первый забирает владение `Arc` и, следовательно, часто требует увеличения счётчика ссылок. Не все типы поддерживают пробуждение по ссылке, поэтому реализация метода `wake_by_ref` является необязательной. Однако это может привести к лучшей производительности, так как избегает ненужных модификаций счётчика ссылок. В нашем случае мы можем просто перенаправить оба метода трейта к нашей функции `wake_task`, которая требует только совместимой ссылки `&self`. ##### Creating Wakers -Since the `Waker` type supports [`From`] conversions for all `Arc`-wrapped values that implement the `Wake` trait, we can now implement the `TaskWaker::new` function that is required by our `Executor::run_ready_tasks` method: +Поскольку тип `Waker` поддерживает преобразования [`From`] для всех значений, обёрнутых в `Arc`, которые реализуют трейт `Wake`, мы теперь можем реализовать функцию `TaskWaker::new`, которая требуется для метода `Executor::run_ready_tasks`: [`From`]: https://doc.rust-lang.org/nightly/core/convert/trait.From.html ```rust -// in src/task/executor.rs +// src/task/executor.rs impl TaskWaker { fn new(task_id: TaskId, task_queue: Arc>) -> Waker { @@ -1681,16 +1681,16 @@ impl TaskWaker { } ``` -We create the `TaskWaker` using the passed `task_id` and `task_queue`. We then wrap the `TaskWaker` in an `Arc` and use the `Waker::from` implementation to convert it to a [`Waker`]. This `from` method takes care of constructing a [`RawWakerVTable`] and a [`RawWaker`] instance for our `TaskWaker` type. In case you're interested in how it works in detail, check out the [implementation in the `alloc` crate][waker-from-impl]. +Мы создаём `TaskWaker`, используя переданные `task_id` и `task_queue`. Затем мы оборачиваем `TaskWaker` в `Arc` и используем реализацию `Waker::from`, чтобы преобразовать его в [`Waker`]. Этот метод `from` заботится о создании экземпляра [`RawWakerVTable`] и [`RawWaker`] для нашего типа `TaskWaker`. Если вам интересно, как это работает в деталях, ознакомьтесь с [реализацией в crate `alloc`][waker-from-impl]. [waker-from-impl]: https://github.com/rust-lang/rust/blob/cdb50c6f2507319f29104a25765bfb79ad53395c/src/liballoc/task.rs#L58-L87 #### A `run` Method -With our waker implementation in place, we can finally construct a `run` method for our executor: +С нашей реализацией waker мы наконец можем создать метод `run` для нашего исполнителя: ```rust -// in src/task/executor.rs +// src/task/executor.rs impl Executor { pub fn run(&mut self) -> ! { @@ -1701,51 +1701,51 @@ impl Executor { } ``` -This method just calls the `run_ready_tasks` function in a loop. While we could theoretically return from the function when the `tasks` map becomes empty, this would never happen since our `keyboard_task` never finishes, so a simple `loop` should suffice. Since the function never returns, we use the `!` return type to mark the function as [diverging] to the compiler. +Этот метод просто вызывает функцию `run_ready_tasks` в цикле. Хотя теоретически мы могли бы выйти из функции, когда карта `tasks` станет пустой, этого никогда не произойдёт, так как наша `keyboard_task` никогда не завершается, поэтому простого `loop` будет достаточено. Поскольку функция никогда не возвращается, мы используем тип возвращаемого значения `!`, чтобы пометить функцию как [расходящуюся][diverging] для компилятора. [diverging]: https://doc.rust-lang.org/stable/rust-by-example/fn/diverging.html -We can now change our `kernel_main` to use our new `Executor` instead of the `SimpleExecutor`: +Теперь мы можем изменить наш `kernel_main`, чтобы использовать наш новый `Executor` вместо `SimpleExecutor`: ```rust -// in src/main.rs +// src/main.rs -use blog_os::task::executor::Executor; // new +use blog_os::task::executor::Executor; // новое fn kernel_main(boot_info: &'static BootInfo) -> ! { - // […] initialization routines, including init_heap, test_main + // […] инициализация всякого, включая init_heap, test_main - let mut executor = Executor::new(); // new + let mut executor = Executor::new(); // новое executor.spawn(Task::new(example_task())); executor.spawn(Task::new(keyboard::print_keypresses())); executor.run(); } ``` -We only need to change the import and the type name. Since our `run` function is marked as diverging, the compiler knows that it never returns, so we no longer need a call to `hlt_loop` at the end of our `kernel_main` function. +Нам нужно только изменить импорт и имя типа. Поскольку наша функция `run` помечена как расходящаяся, компилятор знает, что она никогда не возвращается, поэтому нам больше не нужно вызывать `hlt_loop` в конце функции `kernel_main`. -When we run our kernel using `cargo run` now, we see that keyboard input still works: +Когда мы теперь запускаем наше ядро с помощью `cargo run`, мы видим, что ввод с клавиатуры всё ещё работает: -![QEMU printing ".....H...e...l...l..o..... ...a..g..a....i...n...!"](qemu-keyboard-output-again.gif) +![QEMU печатает ".....H...e...l...l..o..... ...a..g..a....i...n...!"](qemu-keyboard-output-again.gif) -However, the CPU utilization of QEMU did not get any better. The reason for this is that we still keep the CPU busy the whole time. We no longer poll tasks until they are woken again, but we still check the `task_queue` in a busy loop. To fix this, we need to put the CPU to sleep if there is no more work to do. +Тем не менее загрузка процессора QEMU не уменьшилась. Причина в том, что мы по-прежнему загружаем процессор всё время. Мы больше не опрашиваем задачи, пока они не будут пробуждены снова, но мы всё же проверяем `task_queue` в цикле с занятым ожиданием. Чтобы это исправить, нам нужно перевести процессор в спящий режим, если больше нет работы. #### Sleep If Idle -The basic idea is to execute the [`hlt` instruction] when the `task_queue` is empty. This instruction puts the CPU to sleep until the next interrupt arrives. The fact that the CPU immediately becomes active again on interrupts ensures that we can still directly react when an interrupt handler pushes to the `task_queue`. +Основная идея в том, чтобы выполнять [инструкцию `hlt`][[`hlt` instruction]] при пустой `task_queue`. Эта инструкция ставит процессор в спящий режим до следующего прерывания. Факт, что процессор немедленно активируется снова при возникновении прерывания, обеспечивает возможность прямой реакции, когда обработчик прерываний добавляет задачу в `task_queue`. [`hlt` instruction]: https://en.wikipedia.org/wiki/HLT_(x86_instruction) -To implement this, we create a new `sleep_if_idle` method in our executor and call it from our `run` method: +Для реализации этого мы создаём новый метод `sleep_if_idle` в нашем исполнителе и вызываем его из метода `run`: ```rust -// in src/task/executor.rs +// src/task/executor.rs impl Executor { pub fn run(&mut self) -> ! { loop { self.run_ready_tasks(); - self.sleep_if_idle(); // new + self.sleep_if_idle(); // новое } } @@ -1757,30 +1757,30 @@ impl Executor { } ``` -Since we call `sleep_if_idle` directly after `run_ready_tasks`, which loops until the `task_queue` becomes empty, checking the queue again might seem unnecessary. However, a hardware interrupt might occur directly after `run_ready_tasks` returns, so there might be a new task in the queue at the time the `sleep_if_idle` function is called. Only if the queue is still empty, do we put the CPU to sleep by executing the `hlt` instruction through the [`instructions::hlt`] wrapper function provided by the [`x86_64`] crate. +Поскольку мы вызываем `sleep_if_idle` сразу после `run_ready_tasks`, который циклично выполняется до тех пор, пока `task_queue` не станет пустой, проверка очереди может показаться ненужной. Однако аппаратное прерывание может произойти сразу после того, как `run_ready_tasks` возвращает, поэтому в момент вызова функции `sleep_if_idle` может оказаться новая задача в очереди. Только если очередь всё ещё пуста, мы ставим процессор в спящий режим, выполняя инструкцию `hlt` через обёрточную функцию [`instructions::hlt`], предоставляемую библиотекой [`x86_64`]. [`instructions::hlt`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/fn.hlt.html [`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/index.html -Unfortunately, there is still a subtle race condition in this implementation. Since interrupts are asynchronous and can happen at any time, it is possible that an interrupt happens right between the `is_empty` check and the call to `hlt`: +К сожалению, в этой реализации всё ещё присутствует небольшой race condition. Т.к. прерывания асинхронные и могут происходить в любое время, возможно, что прерывание произойдёт сразу между проверкой `is_empty` и вызовом `hlt`: ```rust if self.task_queue.is_empty() { - /// <--- interrupt can happen here + /// <--- прерывание может быть тут x86_64::instructions::hlt(); } ``` -In case this interrupt pushes to the `task_queue`, we put the CPU to sleep even though there is now a ready task. In the worst case, this could delay the handling of a keyboard interrupt until the next keypress or the next timer interrupt. So how do we prevent it? +Если это прерывание добавляет задачу в `task_queue`, мы ставим процессор в спящий режим, даже несмотря на то, что теперь есть готовая задача. В худшем случае это может задержать обработку прерывания клавиатуры до следующего нажатия клавиши или следующего таймерного прерывания. Как же нам этого избежать? -The answer is to disable interrupts on the CPU before the check and atomically enable them again together with the `hlt` instruction. This way, all interrupts that happen in between are delayed after the `hlt` instruction so that no wake-ups are missed. To implement this approach, we can use the [`interrupts::enable_and_hlt`][`enable_and_hlt`] function provided by the [`x86_64`] crate. +Ответ заключается в том, чтобы отключить прерывания на процессоре перед проверкой и атомарно включить их снова вместе с инструкцией `hlt`. Таким образом, все прерывания, которые происходят между этими действиями, будут отложены после инструкции `hlt`, чтобы не пропустить никаких пробуждений. Для реализации этого подхода мы можем использовать функцию [`interrupts::enable_and_hlt`][`enable_and_hlt`], предоставляемую библиотекой [`x86_64`]. [`enable_and_hlt`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/interrupts/fn.enable_and_hlt.html -The updated implementation of our `sleep_if_idle` function looks like this: +Обновлённая реализация нашей функции `sleep_if_idle` выглядит следующим образом: ```rust -// in src/task/executor.rs +// src/task/executor.rs impl Executor { fn sleep_if_idle(&self) { @@ -1796,18 +1796,18 @@ impl Executor { } ``` -To avoid race conditions, we disable interrupts before checking whether the `task_queue` is empty. If it is, we use the [`enable_and_hlt`] function to enable interrupts and put the CPU to sleep as a single atomic operation. In case the queue is no longer empty, it means that an interrupt woke a task after `run_ready_tasks` returned. In that case, we enable interrupts again and directly continue execution without executing `hlt`. +Чтобы избежать состояний гонки, мы отключаем прерывания перед проверкой, пуста ли `task_queue`. Если она пуста, мы используем функцию [`enable_and_hlt`], чтобы включить прерывания и поставить процессор в спящий режим в рамках одной атомарной операции. Если очередь больше не пуста, это означает, что прерывание пробудило задачу после возврата `run_ready_tasks`. В этом случае мы снова включаем прерывания и продолжаем выполнение, не выполняя `hlt`. -Now our executor properly puts the CPU to sleep when there is nothing to do. We can see that the QEMU process has a much lower CPU utilization when we run our kernel using `cargo run` again. +Теперь наш исполнитель правильно ставит процессор в спящий режим, когда задач нету. Мы можем видеть, что загрузка процессора QEMU значительно снизилась, когда мы снова запускаем наше ядро с помощью `cargo run`. #### Possible Extensions -Our executor is now able to run tasks in an efficient way. It utilizes waker notifications to avoid polling waiting tasks and puts the CPU to sleep when there is currently no work to do. However, our executor is still quite basic, and there are many possible ways to extend its functionality: +Наш исполнитель теперь способен эффективно выполнять задачи. Он использует уведомления waker, чтобы избежать опроса ожидающих задач, и переводит процессор в спящий режим, когда задач нету. Однако наш исполнитель всё ещё довольно примитивный, и существует множество способов расширить его функциональность: -- **Scheduling**: For our `task_queue`, we currently use the [`VecDeque`] type to implement a _first in first out_ (FIFO) strategy, which is often also called _round robin_ scheduling. This strategy might not be the most efficient for all workloads. For example, it might make sense to prioritize latency-critical tasks or tasks that do a lot of I/O. See the [scheduling chapter] of the [_Operating Systems: Three Easy Pieces_] book or the [Wikipedia article on scheduling][scheduling-wiki] for more information. -- **Task Spawning**: Our `Executor::spawn` method currently requires a `&mut self` reference and is thus no longer available after invoking the `run` method. To fix this, we could create an additional `Spawner` type that shares some kind of queue with the executor and allows task creation from within tasks themselves. The queue could be the `task_queue` directly or a separate queue that the executor checks in its run loop. -- **Utilizing Threads**: We don't have support for threads yet, but we will add it in the next post. This will make it possible to launch multiple instances of the executor in different threads. The advantage of this approach is that the delay imposed by long-running tasks can be reduced because other tasks can run concurrently. This approach also allows it to utilize multiple CPU cores. -- **Load Balancing**: When adding threading support, it becomes important to know how to distribute the tasks between the executors to ensure that all CPU cores are utilized. A common technique for this is [_work stealing_]. +- **Планирование**: Для нашей `task_queue` мы в настоящее время используем тип [`VecDeque`] для реализации стратегии _первый пришёл — первый вышел_ (FIFO), которая часто также называется _круговым_ планированием. Эта стратегия может быть не самой эффективной для всех нагрузок. Например, имеет смысл приоритизировать задачи с критической задержкой или задачи, выполняющие много ввода-вывода. Для получения дополнительной информации смотрите [главу о планировании][scheduling chapter] книги [_Operating Systems: Three Easy Pieces_] или [статью в Википедии о планировании][scheduling-wiki]. +- **Создание задач**: Сейчас метод `Executor::spawn` требует ссылки `&mut self`, и поэтому он недоступен после вызова метода `run`. Чтобы это исправить, мы могли бы создать дополнительный тип `Spawner`, который делит какую-то очередь с исполнителем и позволяет создавать задачи изнутри самих задач. Очередь может быть `task_queue` напрямую или отдельной очередью, которую исполнитель проверяет в своём цикле выполнения. +- **Использование потоков**: У нас пока нет поддержки потоков, но мы добавим её в следующем посте. Это сделает возможным запуск нескольких экземпляров исполнителя в разных потоках. Преимущество этого подхода заключается в том, что задержка, вызванная длительными задачами, может быть уменьшена, так как другие задачи могут выполняться параллельно. Этот подход также позволяет использовать несколько ядер процессора. +- **Балансировка нагрузки**: При добавлении поддержки потоков становится важно быть в курсе, как распределяются задачи между исполнителями, чтобы обеспечить использование всех ядер процессора. Распространённой техникой для этого является [_work stealing_]. [scheduling chapter]: http://pages.cs.wisc.edu/~remzi/OSTEP/cpu-sched.pdf [_Operating Systems: Three Easy Pieces_]: http://pages.cs.wisc.edu/~remzi/OSTEP/ @@ -1816,18 +1816,18 @@ Our executor is now able to run tasks in an efficient way. It utilizes waker not ## Summary -We started this post by introducing **multitasking** and differentiating between _preemptive_ multitasking, which forcibly interrupts running tasks regularly, and _cooperative_ multitasking, which lets tasks run until they voluntarily give up control of the CPU. +Мы начали этот пост с введения в **мультизадачность** и различия между _вытесняемой_ мультизадачностью, которая регулярно принудительно прерывает выполняющиеся задачи, и _кооперативной_ мультизадачностью, которая позволяет задачам выполняться до тех пор, пока они добровольно не отдадут управление процессором. -We then explored how Rust's support of **async/await** provides a language-level implementation of cooperative multitasking. Rust bases its implementation on top of the polling-based `Future` trait, which abstracts asynchronous tasks. Using async/await, it is possible to work with futures almost like with normal synchronous code. The difference is that asynchronous functions return a `Future` again, which needs to be added to an executor at some point in order to run it. +Затем мы исследовали, как поддержка Rust **async/await** предоставляет реализацию кооперативной мультизадачности на уровне языка. Rust основывает свою реализацию на поллинговом трейте `Future`, который абстрагирует асинхронные задачи. С использованием async/await возможно работать с futures почти так же, как с обычным синхронным кодом. Разница заключается в том, что асинхронные функции снова возвращают `Future`, который в какой-то момент должен быть добавлен в исполнитель для запуска. -Behind the scenes, the compiler transforms async/await code to _state machines_, with each `.await` operation corresponding to a possible pause point. By utilizing its knowledge about the program, the compiler is able to save only the minimal state for each pause point, resulting in a very small memory consumption per task. One challenge is that the generated state machines might contain _self-referential_ structs, for example when local variables of the asynchronous function reference each other. To prevent pointer invalidation, Rust uses the `Pin` type to ensure that futures cannot be moved in memory anymore after they have been polled for the first time. +За кулисами компилятор преобразует код async/await в _конечный автомат_, при этом каждая операция `.await` соответствует возможной точке остановки. Используя свои знания о программе, компилятор может сохранять только минимальное состояние для каждой точки остановки, что приводит к очень низкому потреблению памяти на задачу. Одной из проблем является то, что сгенерированные автоматы могут содержать _самоссылающиеся_ структуры, например, когда локальные переменные асинхронной функции ссылаются друг на друга. Чтобы избежать недействительных указателей, Rust использует тип `Pin`, чтобы гарантировать, что futures не могут быть перемещены в памяти после их первого опроса. -For our **implementation**, we first created a very basic executor that polls all spawned tasks in a busy loop without using the `Waker` type at all. We then showed the advantage of waker notifications by implementing an asynchronous keyboard task. The task defines a static `SCANCODE_QUEUE` using the mutex-free `ArrayQueue` type provided by the `crossbeam` crate. Instead of handling keypresses directly, the keyboard interrupt handler now puts all received scancodes in the queue and then wakes the registered `Waker` to signal that new input is available. On the receiving end, we created a `ScancodeStream` type to provide a `Future` resolving to the next scancode in the queue. This made it possible to create an asynchronous `print_keypresses` task that uses async/await to interpret and print the scancodes in the queue. +Для нашей **реализации** мы сначала создали очень простой исполнитель, который опрашивает все запущенные задачи в цикле с занятым ожиданием, не используя тип `Waker`. Затем мы продемонстрировали преимущество уведомлений waker, реализовав асинхронную задачу клавиатуры. Задача определяет статический `SCANCODE_QUEUE`, используя неблокирующий тип `ArrayQueue`, предоставленный библиотекой `crossbeam`. Вместо непосредственной обработки нажатий клавиш, обработчик прерываний клавиатуры теперь помещает все полученные скан-коды в очередь и затем пробуждает зарегистрированный `Waker`, чтобы сигнализировать, что новый ввод доступен. На принимающей стороне мы создали тип `ScancodeStream`, чтобы предоставить `Future`, разрешающийся в следующий скан-код в очереди. Это сделало возможным создание асинхронной задачи `print_keypresses`, которая использует async/await для интерпретации и вывода скан-кодов в очереди. -To utilize the waker notifications of the keyboard task, we created a new `Executor` type that uses an `Arc`-shared `task_queue` for ready tasks. We implemented a `TaskWaker` type that pushes the ID of woken tasks directly to this `task_queue`, which are then polled again by the executor. To save power when no tasks are runnable, we added support for putting the CPU to sleep using the `hlt` instruction. Finally, we discussed some potential extensions to our executor, for example, providing multi-core support. +Чтобы использовать уведомления waker для задачи клавиатуры, мы создали новый тип `Executor`, который использует совместно используемую `task_queue` на основе `Arc` для готовых задач. Мы реализовали тип `TaskWaker`, который добавляет идентификаторы разбуженных задач непосредственно в эту `task_queue`, которые затем снова опрашиваются исполнителем. Чтобы сэкономить энергию, когда нет запущенных задач, мы добавили поддержку перевода процессора в спящий режим с использованием инструкции `hlt`. Наконец, мы обсудили некоторые потенциальные расширения для нашего исполнителя, например, предоставление поддержки многопроцессорности. ## What's Next? -Using async/wait, we now have basic support for cooperative multitasking in our kernel. While cooperative multitasking is very efficient, it leads to latency problems when individual tasks keep running for too long, thus preventing other tasks from running. For this reason, it makes sense to also add support for preemptive multitasking to our kernel. +Используя async/await, мы теперь имеем базовую поддержку кооперативной мультизадачности в нашем ядре. Хотя кооперативная мультизадачность очень эффективна, она может привести к проблемам с задержкой, когда отдельные задачи выполняются слишком долго, тем самым препятствуя выполнению других задач. По этой причине имеет смысл также добавить поддержку вытесняющей мультизадачности в наше ядро. -In the next post, we will introduce _threads_ as the most common form of preemptive multitasking. In addition to resolving the problem of long-running tasks, threads will also prepare us for utilizing multiple CPU cores and running untrusted user programs in the future. +В следующем посте мы введём _потоки_ как наиболее распространённую форму вытесняющей мультизадачности. В дополнение к решению проблемы долгозадачных задач, потоки также подготовят нас к использованию нескольких ядер процессора и запуску ненадежных пользовательских программ в будущем. From 3e05269ed2bab5c37ca106d7e2e8d3135a5db0e2 Mon Sep 17 00:00:00 2001 From: TakiMoysha <36836047+TakiMoysha@users.noreply.github.com> Date: Sun, 14 Sep 2025 20:58:07 +0200 Subject: [PATCH 5/9] text revisions --- .../posts/12-async-await/index.ru.md | 228 +++++++++--------- 1 file changed, 117 insertions(+), 111 deletions(-) diff --git a/blog/content/edition-2/posts/12-async-await/index.ru.md b/blog/content/edition-2/posts/12-async-await/index.ru.md index 547b14c8..b6061ac5 100644 --- a/blog/content/edition-2/posts/12-async-await/index.ru.md +++ b/blog/content/edition-2/posts/12-async-await/index.ru.md @@ -13,7 +13,7 @@ translators = ["TakiMoysha"] +++ -В этом посте мы рассмотрим _кооперативную многозадачность_ и возможности _async/await_ в Rust. Мы подробно рассмотрим, как async/await работает в Rust, включая трейт `Future`, преобразование машины состояний и _pinning_. Затем мы добавим базовую поддержку async/await в наше ядро, by creating an asynchronous keyboard task and a basic executor. +В этом посте мы рассмотрим _кооперативную многозадачность_ и возможности _async/await_ в Rust. Мы подробно рассмотрим, как async/await работает в Rust, включая трейт `Future`, переходы в конечных автоматах и _закрепления_ (pinning). Мы добавим базовую поддержку async/await в наше ядро путем создания асинхронных задач обработки ввода с клавиатуры и базовый исполнитель (executor). @@ -28,19 +28,19 @@ translators = ["TakiMoysha"] ## Многозадачность -Одной из основных функций возможностей операционных систем является [_многозадачность_][multitasking], то есть возможность одновременного выполнения нескольких задач. Например, вероятно, пока вы читаете этот пост, у вас открыты другие программы, такие как текстовый редактор или окно терминала. Даже если у вас открыто только одно окно браузера, вероятно, в фоновом режиме выполняются различные задачи по управлению окнами рабочего стола, проверке обновлений или индексированию файлов. +Одной из основных функций возможностей операционных систем является [_многозадачность_][multitasking], то есть возможность одновременного выполнения нескольких задач. Например, вероятно, пока вы читаете этот пост, у вас открыты другие программы, вроде текстового редактора или терминала. Даже если у вас открыт только один браузер, вероятно, в фоновом режиме выполняются различные задачи по управлению окнами рабочего стола, проверке обновлений или индексированию файлов. [_multitasking_]: https://en.wikipedia.org/wiki/Computer_multitasking Хотя кажется, что все задачи выполняются параллельно, на одном ядре процессора может выполняться только одна задача за раз. Чтобы создать иллюзию параллельного выполнения задач, операционная система быстро переключается между активными задачами, чтобы каждая из них могла выполнить небольшой прогресс. Поскольку компьютеры работают быстро, мы в большинстве случаев не замечаем этих переключений. -Когда одноядерные центральные процессоры (ЦП) могут выполнять только одну задачу за раз, многоядерные ЦП могут выполнять несколько задач по настоящему параллельно. Например, процессор с 8 ядрами может выполнять 8 задач одновременно. В следующей статье мы расскажем, как настроить многоядерные ЦП. В этой статье для простоты мы сосредоточимся на одноядерных процессорах. (Стоит отметить, что все многоядерные ЦП запускаются с одним активным ядром, поэтому пока мы можем рассматривать их как одноядерные процессоры). +Одноядерные центральные процессоры (ЦП) могут выполнять только одну задачу за раз, а многоядерные ЦП могут выполнять несколько задач по настоящему параллельно. Например, процессор с 8 ядрами может выполнять 8 задач одновременно. В следующей статье мы расскажем, как настроить многоядерные ЦП . В этой статье для простоты мы сосредоточимся на одноядерных процессорах. (Стоит отметить, что все многоядерные ЦП запускаются с одним активным ядром, поэтому пока мы можем рассматривать их как одноядерные процессоры). -Есть две формы многозадачности: _кооперативная_ (совместная) - требует, чтобы задачи регулярно отдавали контроль над процессором для продвижения других задач; _вытесняющая_ (приоритетная) - использующая функционал операционной системы (ОС) для переключения потоков в произвольные моменты моменты времени через принудительную остановку. Далее мы рассмотрим две формы многозадачности более подробно и обсудим их преимущества и недостатки. +Есть две формы многозадачности: _кооперативная_ или совместная (_cooperative_) - требует, чтобы задачи регулярно отдавали контроль над процессором для продвижения других задач; _вытесняющая_ или приоритетная () _preemptive_) - использующая функционал операционной системы (ОС) для переключения потоков в произвольные моменты моменты времени через принудительную остановку. Далее мы рассмотрим две формы многозадачности более подробно и обсудим их преимущества и недостатки. ### Вытесняющая Многозадачность -Идея заключается в том, что ОС контролирует, когда переключать задачи. Для этого она использует факт того, что при каждом прирывании она восстанавливает контрлоль над ЦП. Это позволяет переключать задачи всякий раз, когда в системе появляется новый ввод. Например, возможность переключать задачи когда двигается мышка или приходят пакеты по сети. ОС также может определять точное время, в течении которого задаче разрешается выполняться, настроив аппаратный таймер на отправку прерывания по истечению этого времени. +Замысел вытесняющей многозадачности в том, что за управление переключением между задачами отвечает ОС. Для этого она использует тот факт, что при каждом прерывании она восстанавливает контроль над ЦП. Это позволяет переключать задачи всякий раз, когда в системе появляется новый ввод. Например, возможность переключать задачи когда двигается мышка или приходят пакеты по сети. ОС также может определять точное время, в течении которого задаче разрешается выполняться, настроив аппаратный таймер на прерывание по истечению этого времени. На следующем рисунку показан процесс переключения задач при аппаратном прерывании: @@ -52,52 +52,52 @@ translators = ["TakiMoysha"] #### Сохранение состояния -Поскольку задачи прерываются в произвольные моменты времени, они могут находиться в середине вычислений. Чтобы иметь возможность возобновить их позже, ОС должна создать копию всего состояния задачи, включая ее [стек вызовов][call stack] и значения всех регистров ЦП. Этот процесс называется [_переключением контекста_][_context switch_]. +Поскольку задачи прерываются в произвольные моменты времени, они могут находиться в середине вычислений. Чтобы иметь возможность возобновить их позже, ОС должна создать копию всего состояния задачи, включая ее [_стек вызовов_] и значения всех регистров ЦП. Этот процесс называется ["_переключение контекста_"]. -[call stack]: https://en.wikipedia.org/wiki/Call_stack -[_context switch_]: https://en.wikipedia.org/wiki/Context_switch +[_стек вызовов_]: https://ru.wikipedia.org/wiki/Стек_вызовов +[_переключение контекста_]: https://ru.wikipedia.org/wiki/Переключение_контекста -Поскольку стек вызовов может быть очень большим, операционная система обычно создает отдельный стек вызовов для каждой задачи, вместо того чтобы сохранять содержимое стека вызовов при каждом переключении задач. Такая задача со своим собственным стеком называется [_потоком выполнения_][_thread of execution_] или сокращенно _поток_. Используя отдельный стек для каждой задачи, при переключении контекста необходимо сохранять только содержимое регистров (включая программный счетчик и указатель стека). Такой подход минимизирует накладные расходы на производительность при переключении контекста, что очень важно, поскольку переключения контекста часто происходят до 100 раз в секунду. +Поскольку стек вызовов может быть очень большим, операционная система обычно создает отдельный стек вызовов для каждой задачи, вместо того чтобы сохранять содержимое стека вызовов при каждом переключении задач. Такая задача со своим собственным стеком называется ["_поток выполнения_"] или сокращенно _поток_. Используя отдельный стек для каждой задачи, при переключении контекста необходимо сохранять только содержимое регистров (включая программный счетчик и указатель стека). Такой подход минимизирует накладные расходы на производительность при переключении контекста, что очень важно, поскольку переключения контекста часто происходят до 100 раз в секунду. -[_thread of execution_]: https://en.wikipedia.org/wiki/Thread_(computing) +[_поток выполнения_]: https://en.wikipedia.org/wiki/Thread_(computing) #### Обсуждение -Основным преимуществом вытесняющей многозадачности является то, что операционная система может полностью контролировать разрешенное время выполнения задачи. Таким образом, она может гарантировать, что каждая задача получит справедливую долю времени процессора, без необходимости полагаться на кооперацию задач. Это особенно важно при выполнении сторонних задач или когда несколько пользователей совместно используют одну систему. +Основным преимуществом вытесняющей многозадачности является то, что операционная система может полностью контролировать время выполнения каждой задачи. Таким образом, ОС может гарантировать, что каждая задача получит справедливую долю времени ЦП, без необходимости полагаться на кооперацию задач. Это особенно важно при выполнении сторонних задач или когда несколько пользователей совместно используют одну систему. -Недостатком вытесняющей многозадачности является то, что каждой задаче требуется собственный стек. По сравнению с общим стеком это приводит к более высокому использованию памяти на задачу и часто ограничивает количество задач в системе. Другим недостатком является то, что ОС всегда должна сохранять полное состояние регистров ЦП при каждом переключении задач, даже если задача использовала только небольшую часть регистров. +Недостатком вытесняющей многозадачности в том, что каждой задаче требуется собственный стек. По сравнению с общим стеком это приводит к более высокому использованию памяти на задачу и часто ограничивает количество задач в системе. Другим недостатком является то, что ОС всегда должна сохранять полное состояние регистров ЦП при каждом переключении задач, даже если задача использовала только небольшую часть регистров. -Вытесняющая многозадачность и потоки - фундаментальные компонтенты ОС, т.к. они позволяют запускать недоверенные программы в userspace (run untrusted userspace programs) . Мы подробнее обсудим эти концепции в будущийх постах. Однако сейчас, мы сосредоточимся на кооперативной многозадачности, которая также предоставляет полезные возможности для нашего ядра. +Вытесняющая многозадачность и потоки - фундаментальные компонтенты ОС, т.к. они позволяют запускать неизвестные программы в userspace . Мы подробнее обсудим эти концепции в будущийх постах. Однако сейчас, мы сосредоточимся на кооперативной многозадачности, которая также предоставляет полезные возможности для нашего ядра. ### Кооперативная Многозадачность Вместо принудительной остановки выполняющихся задач в произвольные моменты времени, кооперативная многозадачность позволяет каждой задаче выполняться до тех пор, пока она добровольно не уступит контроль над ЦП. Это позволяет задачам самостоятельно приостанавливаться в удобные моменты времени, например, когда им нужно ждать операции ввода-вывода. -Кооперативная многозадачность часто используется на языковом уровне, например в виде [сопрограмм][coroutines] или [async/await]. Идея в том, что программист или компилятор вставляет в программу операции [_yield_], которые отказываются от управления ЦП и позволяют выполняться другим задачам. Например, yield может быть вставлен после каждой итерации сложного цикла. +Кооперативная многозадачность часто используется на языковом уровне, например в виде [сопрограмм] или [async/await]. Идея в том, что программист или компилятор вставляет в программу операции [_yield_], которые отказываются от управления ЦП и позволяют выполняться другим задачам. Например, yield может быть вставлен после каждой итерации сложного цикла. -[coroutines]: https://en.wikipedia.org/wiki/Coroutine +[сопрограмм]: https://ru.wikipedia.org/wiki/Сопрограмма [async/await]: https://rust-lang.github.io/async-book/01_getting_started/04_async_await_primer.html [_yield_]: https://en.wikipedia.org/wiki/Yield_(multithreading) -Часто кооперативную многозадачность совмещают с [асинхронными операциями][asynchronous operations]. Вместо того чтобы ждать завершения операции и препятствовать выполнению других задач в это время, асинхронные операции возвращают статус «не готов», если операция еще не завершена. В этом случае ожидающая задача может выполнить операцию yield, чтобы другие задачи могли выполняться. +Часто кооперативную многозадачность совмещают с [асинхронными операциями]. Вместо того чтобы ждать завершения операции и препятствовать выполнению других задач в это время, асинхронные операции возвращают статус «не готов», если операция еще не завершена. В этом случае ожидающая задача может выполнить операцию yield, чтобы другие задачи могли выполняться. -[asynchronous operations]: https://en.wikipedia.org/wiki/Asynchronous_I/O +[асинхронными операциями]: https://ru.wikipedia.org/wiki/Асинхронный_ввод-вывод #### Сохранение состояния Поскольку задачи сами определяют точки паузы, им не нужно, чтобы ОС сохраняла их состояние. Вместо этого они могут сохранять то состояние, которое необходимо для продолжения работы, что часто приводит к улучшению производительности. Например, задаче, которая только что завершила сложные вычисления, может потребоваться только резервное копирование конечного результата вычислений, т.к. промежуточные результаты ей больше не нужны. -Реализации кооперативных задач, поддерживаемые языком, часто даже могут сохранять необходимые части стека вызовов перед приостановкой. Например, реализация async/await в Rust сохраняет все локальные переменные, которые еще нужны, в автоматически сгенерированной структуре (см. ниже). Благодаря резервному копированию соответствующих частей стека вызовов перед приостановкой все задачи могут использовать один стек вызовов, что приводит к значительному снижению потребления памяти на задачу. Это позволяет создавать практически любое количество кооперативных задач без исчерпания памяти. +Поддерживаемые языком реализации кооперативной мультизадачности часто даже могут сохранять необходимые части стека вызовов перед паузой. Например, реализация async/await в Rust сохраняет все локальные переменные, которые еще нужны, в автоматически сгенерированной структуре (см. ниже). Благодаря резервному копированию соответствующих частей стека вызовов перед приостановкой все задачи могут использовать один стек вызовов, что приводит к значительному снижению потребления памяти на задачу. Это позволяет создавать практически любое количество кооперативных задач без исчерпания памяти. #### Обсуждение -Недостатком кооперативной многозадачности является то, что некооперативная задача может потенциально выполняться в течение неограниченного времени. Таким образом, вредоносная или содержащая ошибки задача может помешать выполнению других задач и замедлить или даже заблокировать работу всей системы. По этой причине кооперативная многозадачность должна использоваться только в том случае, если известно, что все задачи будут взаимодействовать друг с другом. В качестве противоположного примера можно привести то, что не стоит полагаться на взаимодействие произвольных программ пользовательского уровня в операционной системе. +Недостатком кооперативной многозадачности является то, что некооперативная задача может потенциально выполняться в течение неограниченного времени. Таким образом, вредоносная или содержащая ошибки задача может помешать выполнению других задач и замедлить или даже заблокировать работу всей системы. По этой причине кооперативная многозадачность должна использоваться только в том случае если известно, что все задачи будут взаимодействовать друг с другом (). Как контрпример, не стоит полагаться на взаимодействие произвольных программ пользовательского пространства (user-level) в ОС. Однако высокая производительность и преимущества кооперативной многозадачности в плане памяти делают ее хорошим подходом для использования внутри программы, особенно в сочетании с асинхронными операциями. Поскольку ядро операционной системы является программой, критичной с точки зрения производительности, которая взаимодействует с асинхронным оборудованием, кооперативная многозадачность кажется хорошим подходом для реализации параллелизма. ## Async/Await в Rust -Rust предоставляет отличную поддержку кооперативной многозадачности в виде async/await. Прежде чем мы сможем изучить, что такое async/await и как оно работает, нам необходимо понять, как работают _futures_ и асинхронное программирование в Rust. +Rust предоставляет отличную поддержку кооперативной многозадачности в виде async/await. Прежде чем мы сможем изучить, что такое async/await и как оно работает, нам необходимо понять, как работают _futures_ (футуры) и асинхронное программирование в Rust. ### Futures @@ -105,9 +105,9 @@ _Future_ представляет значение, которое может б #### Пример -Концепцию future лучше всего проиллюстрировать небольшим примером: +Концепцию футур лучше всего проиллюстрировать небольшим примером: -![Диаграмма последовательности: main вызывает `read_file` и блокируется до его возврата; затем вызывает `foo()` и также блокируется до его возврата. Тот же процесс повторяется, но на этот раз вызывается `async_read_file`, который сразу возвращает future; затем снова вызывается `foo()`, который теперь выполняется одновременно с загрузкой файла. Файл становится доступным до возврата `foo()`.](async-example.svg) +![Sequence diagram (Диаграмма последовательности): main вызывает `read_file` и блокируется до его возврата; затем вызывает `foo()` и также блокируется до его возврата. Тот же процесс повторяется, но на этот раз вызывается `async_read_file`, который сразу возвращает future; затем снова вызывается `foo()`, который теперь выполняется одновременно с загрузкой файла. Файл становится доступным до возврата `foo()`.](async-example.svg) Эта диаграмма последовательности показывает функцию `main`, которая считывает файл из файловой системы, а затем вызывает функцию `foo`. Этот процесс повторяется дважды: один раз с синхронным вызовом `read_file` и один раз с асинхронным вызовом `async_read_file`. @@ -144,9 +144,9 @@ pub enum Poll { } ``` -Когда значение уже доступно (например, файл был полностью прочитан с диска), оно возвращается, обернутое в вариант `Ready`. Иначе возвращается вариант `Pending`, который сигнализирует вызывающему, что значение еще не доступно. +Когда значение уже доступно (например, файл был полностью прочитан с диска), оно возвращается, обернутое в `Ready`. Иначе возвращается `Pending`, который сигнализирует вызывающему, что значение еще не доступно. -Метод `poll` принимает два аргумента: `self: Pin<&mut Self>` и `cx: &mut Context`. Первый аргумент ведет себя аналогично обычной ссылке `&mut self`, за исключением того, что значение `Self` [_pinned_] к своему месту в памяти. Понять `Pin` и его необходимость сложно, не понимая сначала, как работает async/await. Поэтому мы объясним это позже в этом посте. +Метод `poll` принимает два аргумента: `self: Pin<&mut Self>` и `cx: &mut Context`. Первый аргумент ведет себя аналогично обычной ссылке `&mut self`, за исключением того, что значение `Self` [_закрепленно_][_pinned_] (pinned) к своему месту в памяти. Понять концепцию закрепления (`Pin`) и его необходимость сложно, если не понимать как работает async/await. Поэтому мы объясним это позже в этом посте. [_pinned_]: https://doc.rust-lang.org/nightly/core/pin/index.html @@ -154,11 +154,11 @@ pub enum Poll { [`Waker`]: https://doc.rust-lang.org/nightly/core/task/struct.Waker.html -### Working with Futures +### Работа с Futures Теперь мы знаем, как определяются футуры, и понимаем основную идею метода `poll`. Однако мы все еще не знаем, как эффективно работать с футурами. Проблема в том, что они представляют собой результаты асинхронных задач, которые могут быть еще недоступны. На практике, однако, нам часто нужны эти значения непосредственно для дальнейших вычислений. Поэтому возникает вопрос: как мы можем эффективно получить значение, когда оно нам нужно? -#### Waiting on Futures +#### Ожидание Futures Один из возможных ответов — дождаться, пока футура исполнится. Это может выглядеть примерно так: @@ -177,7 +177,8 @@ let file_content = loop { Более эффективным подходом может быть _блокировка_ текущего потока до тех пор, пока футура не станет доступной. Конечно, это возможно только при наличии потоков, поэтому это решение не работает для нашего ядра, по крайней мере, пока. Даже в системах, где поддерживается блокировка, она часто нежелательна, поскольку превращает асинхронную задачу в синхронную, тем самым сдерживая потенциальные преимущества параллельных задач в плане производительности. #### Комбинаторы Future -Альтернативой ожиданию является использование комбинаторов future. _Комбинаторы future_ - это методы вроде `map`, которые позволяют объединять и связывать future между собой, аналогично методам трейта [`Iterator`]. Вместо того чтобы ожидать выполнения future, эти комбинаторы сами возвращают future, которые применяет операцию преобразования при вызове `poll`. + +Альтернативой ожиданию является использование комбинаторов футур. _Комбинаторы future_ - это методы вроде `map`, которые позволяют объединять и связывать future между собой, аналогично методам трейта [`Iterator`]. Вместо того чтобы ожидать выполнения future, эти комбинаторы сами возвращают future, которые применяет операцию преобразования при вызове `poll`. [`Iterator`]: https://doc.rust-lang.org/stable/core/iter/trait.Iterator.html @@ -214,23 +215,29 @@ fn file_len() -> impl Future { } ``` -Этот код не совсем корректен, потому что не учитывает [_pinning_], но он подходит для примера. Основная идея в том, что функция `string_len` оборачивает переданный экземпляр `Future` в новую структуру `StringLen`, которая также реализует `Future`. При опросе обёрнутого future опрашивается внутренний future. Если значение ещё не готово, из обёрнутого future также возвращается `Poll::Pending`. Если значение готово, строка извлекается из варианта `Poll::Ready`, вычисляется её длина, после чего результат снова оборачивается в `Poll::Ready` и возвращается. +Этот код не совсем корректен, потому что не учитывает [_закрепление_][_pinning_], но он подходит для примера. Суть в том, что функция `string_len` оборачивает переданный экземпляр `Future` в новую структуру `StringLen`, которая также реализует `Future`. При опросе футуры-обертки опрашивается внутренняя футура. Если значение ещё не готово, из футуры-обертки возвращается `Poll::Pending`. Если значение готово, строка извлекается из `Poll::Ready`, вычисляется её длина, после чего результат снова оборачивается в `Poll::Ready` и возвращается. [_pinning_]: https://doc.rust-lang.org/stable/core/pin/index.html -С помощью функции `string_len` можно вычислить длину асинхронной строки, не дожидаясь её завершения. Поскольку функция снова возвращает `Future`, вызывающий код не может работать с возвращённым значением напрямую, а должен использовать комбинаторы. Таким образом, весь граф вызовов становится асинхронным, и в какой-то момент (например, в основной функции) можно эффективно ожидать завершения нескольких future одновременно. +С помощью функции `string_len` можно вычислить длину асинхронной строки, не дожидаясь её завершения. Поскольку функция снова возвращает `Future`, вызывающий код не может работать с возвращённым значением напрямую, а должен использовать комбинаторы. Таким образом, весь граф вызовов становится асинхронным, и в какой-то момент (например, в основной функции) можно эффективно ожидать завершения нескольких футур одновременно. Так как ручное написание функций-комбинаторов сложно, они обычно предоставляются библиотеками. Стандартная библиотека Rust пока не содержит методов-комбинаторов, но полуофициальная (и совместимая с `no_std`) библиотека [`futures`] предоставляет их. Её трейт [`FutureExt`] включает высокоуровневые методы-комбинаторы, такие как [`map`] или [`then`], которые позволяют манипулировать результатом с помощью произвольных замыканий. +[`futures`]: https://docs.rs/futures/0.3.4/futures/ +[`FutureExt`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html +[`map`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html#method.map +[`then`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html#method.then + ##### Преимущества -Большое преимущество future комбинаторов (future combinators) в том, что они сохраняют асинхронность. В сочетании с асинхронными интерфейсами ввода-вывода такой подход может обеспечить очень высокую производительность. То, что future кобинаторы реализованы как обычные структуры с имплементацией трейтов, позволяет компилятору чрезвычайно оптимизировать их. Подробнее см. в посте [_Futures с нулевой стоимостью в Rust_], где было объявлено о добавлении futures в экосистему Rust. +Большое преимущество комбинаторов футур (future combinators) в том, что они сохраняют асинхронность. В сочетании с асинхронными интерфейсами ввода-вывода такой подход может обеспечить очень высокую производительность. То, что future кобинаторы реализованы как обычные структуры с имплементацией трейтов, позволяет компилятору чрезвычайно оптимизировать их. Подробнее см. в посте [_Futures с нулевой стоимостью в Rust_], где было объявлено о добавлении futures в экосистему Rust. [_Futures с нулевой стоимостью в Rust_]: https://aturon.github.io/blog/2016/08/11/futures/ ##### Недостатки -Хотя future комбинаторы позволяют писать очень эффективный код, их может быть сложно использовать в некоторых ситуациях из-за системы типов и интерфейса на основе замыканий. Например, рассмотрим такой код: +Хотя комбинаторы футур позволяют писать очень эффективный код, их может быть сложно использовать в некоторых ситуациях из-за системы типов и интерфейса на основе замыканий. Например, рассмотрим такой код: + ```rust fn example(min_len: usize) -> impl Future { async_read_file("foo.txt").then(move |content| { @@ -243,12 +250,11 @@ fn example(min_len: usize) -> impl Future { } ``` - -([Попробовать в песочнице](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=91fc09024eecb2448a85a7ef6a97b8d8)) +([Попробовать](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=91fc09024eecb2448a85a7ef6a97b8d8)) Здесь мы читаем файл `foo.txt`, а затем используем комбинатор [`then`], чтобы связать вторую футуру на основе содержимого файла. Если длина содержимого меньше заданного `min_len`, мы читаем другой файл `bar.txt` и добавляем его к `content` с помощью комбинатора [`map`]. В противном случае возвращаем только содержимое `foo.txt`. -Нам нужно использовать ключевое слово [`move`] для замыкания, передаваемого в `then`, иначе возникнет ошибка времени жизни (lifetime) для `min_len`. Причина использования обёртки [`Either`] заключается в том, что блоки `if` и `else` всегда должны возвращать значения одного типа. Поскольку в блоках возвращаются разные типы будущих значений, нам необходимо использовать обёртку, чтобы привести их к единому типу. Функция [`ready`] оборачивает значение в будущее, которое сразу готово к использованию. Здесь она необходима, потому что обёртка `Either` ожидает, что обёрнутое значение реализует `Future`. +Нам нужно использовать ключевое слово [`move`] для замыкания, передаваемого в `then`, иначе возникнет ошибка времени жизни (lifetime) для `min_len`. Причина использования обёртки [`Either`] в том, что блоки `if` и `else` всегда должны возвращать значения одного типа. Поскольку возвращаются разные типы футур в блоке, нам необходимо использовать тип-обертку, чтобы привести их к единому типу. Функция [`ready`] оборачивает значение в футуру, которая сразу готова к использованию. Здесь она необходима, потому что обёртка `Either` ожидает, что обёрнутое значение реализует `Future`. [`move` keyword]: https://doc.rust-lang.org/std/keyword.move.html [`Either`]: https://docs.rs/futures/0.3.4/futures/future/enum.Either.html @@ -296,13 +302,13 @@ async fn example(min_len: usize) -> String { } ``` -([Попробовать в песочнице](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d93c28509a1c67661f31ff820281d434)) +([Попробовать](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d93c28509a1c67661f31ff820281d434)) -Эта ф-ция - прямой перевод `example` написанной [выше](#Недостатки), которая использовала комбинаторные ф-ции. Используя оператор `.await`, мы можем получить значение future без необходимости использования каких-либо замыканий или типов `Either`. В результате, мы можем писать наш код так же, как если бы это был обычный синхронный код, с той лишь разницей, что _это все еще асинхронный код_. +Эта ф-ция - прямой перевод `example` написанной [выше](#Недостатки), которая использовала комбинаторы. Используя оператор `.await`, мы можем получить значение футуры без необходимости использования каких-либо замыканий или типов `Either`. В результате, мы можем писать наш код так же, как если бы это был обычный синхронный код, с той лишь разницей, что _это все еще асинхронный код_. -#### Преобразованиe Конечных Автоматов (Машина состояний) +#### Преобразованиe Конечных Автоматов -За кулисами компилятор преобразует тело ф-ции `async` в [_state machine_] с каждым вызовом `.await`, представляющим собой разное состояние. Для вышеуказанной ф-ции `example`, компилятор создает state machine с четырьмя состояниями. +За кулисами компилятор преобразует тело функции `async` в [_конечный автомат_][_state machine_] (state machine) с каждым вызовом `.await`, представляющим собой разное состояние. Для вышеуказанной ф-ции `example`, компилятор создает конечный автомат с четырьмя состояниями. [_state machine_]: https://en.wikipedia.org/wiki/Finite-state_machine @@ -314,7 +320,7 @@ async fn example(min_len: usize) -> String { ![Четыре состояния и переходы: start, waiting on foo.txt, waiting on bar.txt, end](async-state-machine-basic.svg) -Диаграмма использует стрелки для представления переключений состояний и ромбы для представления альтернативных путей. Например, если файл `foo.txt` не готов, то мы используется путь _"no"_ переходя в состояние _"waiting on foo.txt"_. Иначе, используется путь _"да"_. Где маленький красный комб без подписи - ветвь ф-ции exmple, где `if content.len() < 100`. +Диаграмма использует стрелки для представления переключений состояний и ромбы для представления альтернативных путей. Например, если файл `foo.txt` не готов, то мы используется путь _"no"_ переходя в состояние _"waiting on foo.txt"_. Иначе, используется путь _"yes"_. Где маленький красный ромб без подписи - ветвь ф-ции exmple, где `if content.len() < 100`. Мы видим, что первый вызов `poll` запускает функцию и она выполняться до тех пор, пока у футуры не будет результата. Если все футуры на пути готовы, ф-ция может выполниться до состояния _"end"_ , то есть вернуть свой результат, завернутый в `Poll::Ready`. В противном случае конечный автомат переходит в состояние ожидания и возвращает `Poll::Pending`. При следующем вызове `poll` машина состояний начинает с последнего состояния ожидания и повторяет последнюю операцию. @@ -323,6 +329,7 @@ async fn example(min_len: usize) -> String { Для продолжнеия работы с последнего состояния ожидания, автомат должен отслеживать текущее состояние внутри себя. Еще, он должен сохранять все переменные, которые необходимы для продолжнеия выполнения при следующем вызове `poll`. Здесь компилятор действительно может проявить себя: зная, когда используются те или иные переменные, он может автоматически создавать структуры с точным набором требуемых переменных. Например, компилятор генерирует структуры для вышеприведенной ф-ции `example`: + ```rust // снова `example` что бы вам не пришлось прокручивать вверх async fn example(min_len: usize) -> String { @@ -353,7 +360,7 @@ struct WaitingOnBarTxtState { struct EndState {} ``` -В состояниях "start" и _"waiting on foo.txt"_ необходимо сохранить параметр `min_len` для последующего сравнения с `content.len()`. Состояние _"waiting on foo.txt"_ дополнительно содержит `foo_txt_future`, представляющий future возвращаемое вызовом `async_read_file`. Этe футуру нужно опросить снова, когда автомат продолжит свою работу, поэтому его нужно сохранить. +В состояниях "start" и _"waiting on foo.txt"_ необходимо сохранить параметр `min_len` для последующего сравнения с `content.len()`. Состояние _"waiting on foo.txt"_ дополнительно содержит `foo_txt_future`, представляющий future возвращаемое вызовом `async_read_file`. Эту футуру нужно опросить снова, когда автомат продолжит свою работу, поэтому его нужно сохранить. Состояние "waiting on bar.txt" содержит переменную `content` для последующей конкатенации строк при загрузке файла `bar.txt`. Оно также хранит `bar_txt_future`, представляющее текущую загрузку файла `bar.txt`. Эта структура не содержит переменную `min_len`, потому что она уже не нужна после проверки длины строки `content.len()`. В состоянии _"end"_, в структуре ничего нет, т.к. ф-ция завершилась полностью. @@ -374,7 +381,7 @@ enum ExampleStateMachine { } ``` -Мы определяем отдельный вариант перечисления (enum) для каждого состояния и добавляем соответствующую структуру состояния в каждый вариант как поле. Чтобы реализовать переходы между состояниями, компилятор генерирует реализацию trait'а `Future` на основе функции `example`: +Мы определяем отдельный вариант перечисления (enum) для каждого состояния и добавляем соответствующую структуру состояния в каждый вариант как поле. Чтобы реализовать переходы между состояниями, компилятор генерирует реализацию трейта `Future` на основе функции `example`: ```rust impl Future for ExampleStateMachine { @@ -393,11 +400,11 @@ impl Future for ExampleStateMachine { } ``` -Тип `Output` будущего равен `String`, потому что это тип возвращаемого значения функции `example`. Для реализации метода `poll` мы используем условную инструкцию `match` на текущем состоянии внутри цикла. Идея в том, что мы переходим к следующему состоянию, пока это возможно, и явно возвращаем `Poll::Pending`, когда мы не можем продолжить. +Тип для `Output` указан `String`, потому что этот тип возвращает функция `example`. Для реализации метода `poll` мы используем условную инструкцию `match` на текущем состоянии внутри цикла. Идея в том, что мы переходим к следующему состоянию, пока это возможно, и явно возвращаем `Poll::Pending`, когда мы не можем продолжить. -Для упрощения мы представляем только упрощенный код и не обрабатываем [закрепление][_pinning_], владения, lifetimes, и т.д. Поэтому этот и следующий код должны быть восприняты как псевдокод и не использоваться напрямую. Конечно, реальный генерируемый компилятором код обрабатывает всё верно, хотя возможно это будет сделано по-другому. +Для упрощения мы представляем только упрощенный код и не обрабатываем [закрепление][_pinning_], владения, время жизни, и т.д. Поэтому этот и следующий код должны быть восприняты как псевдокод и не использоваться напрямую. Конечно, реальный генерируемый компилятором код обрабатывает корректно, хотя возможно это будет сделано по-другому. -Чтобы сохранить примеры кода маленькими, мы представляем код для каждого варианта `match` отдельно. Начнем с состояния `Start`: +Чтобы сохранить примеры кода маленькими, мы напишем код для каждой ветки `match` отдельно. Начнем с состояния `Start`: ```rust ExampleStateMachine::Start(state) => { @@ -411,9 +418,10 @@ ExampleStateMachine::Start(state) => { *self = ExampleStateMachine::WaitingOnFooTxt(state); } ``` -Машина состояний находится в состоянии `Start`, когда она прямо в начале функции. В этом случае выполняем весь код из тела функции `example` до первого `.await`. Чтобы обработать операцию `.await`, мы меняем состояние машины на `WaitingOnFooTxt`, которое включает в себя построение структуры `WaitingOnFooTxtState`. -Пока `match self {…}` выполняется в цилке, выполнение прыгает к `WaitingOnFooTxt`: +Автомат находится в состоянии `Start`, когда ф-ция только начинает выполнение. В этом случае выполняем весь код из тела функции `example` до первого `.await`. Чтобы обработать операцию `.await`, мы меняем состояние на `WaitingOnFooTxt`, которое включает в себя построение структуры `WaitingOnFooTxtState`. + +Пока `match self {…}` выполняется в цикле, выполнение переходит к `WaitingOnFooTxt`: ```rust ExampleStateMachine::WaitingOnFooTxt(state) => { @@ -438,9 +446,9 @@ ExampleStateMachine::WaitingOnFooTxt(state) => { } ``` -В этом варианте `match`, вначале мы вызываем функцию `poll` для `foo_txt_future`. Если она не готова, мы выходим из цикла и возвращаем `Poll::Pending`. В этом случае `self` остается в состоянии `WaitingOnFooTxt`, следующий вызов функции `poll` на машине состояний попадёт в тот же `match` и повторит проверку готовности `foo_txt_future`. +Эта ветка `match` начинавется с вызова `poll` для `foo_txt_future`. Если она не готова, мы выходим из цикла и возвращаем `Poll::Pending`. В этом случае `self` остается в состоянии `WaitingOnFooTxt`, следующий опрос `poll` автомата попадёт в ту же ветку `match` и повторит проверку готовности `foo_txt_future`. -Когда `foo_txt_future` готов, мы присваиваем результат переменной `content` и продолжаем выполнять код функции `example`: Если `content.len()` меньше сохранённого в структуре состояния `min_len`, файл `bar.txt` читается асинхронно. Мы ещё раз переводим операцию `.await` в изменение состояния, теперь в состояние `WaitingOnBarTxt`. Следуя за выполнением `match` внутри цикла, выполнение прямо переходит к варианту `match` для нового состояния позже, где проверяется готовность `bar_txt_future`. +Когда `foo_txt_future` готов, мы присваиваем результат переменной `content` и продолжаем выполнять код функции `example`: Если `content.len()` меньше чем `min_len`, из структуры состояния, файл `bar.txt` читается асинхронно. Мы ещё раз переводим операцию `.await` в изменение состояния, теперь в состояние `WaitingOnBarTxt`. Пока мы выполняем `match` внутри цикла, исполнение позже переходит к ветке `match` нового состояния, где проверяется готовность `bar_txt_future`. В случае входа в ветку `else`, более никаких операций `.await` не происходит. Мы достигаем конца функции и возвращаем `content` обёрнутую в `Poll::Ready`. Также меняем текущее состояние на `End`. @@ -459,27 +467,29 @@ ExampleStateMachine::WaitingOnBarTxt(state) => { } ``` -Аналогично состоянию `WaitingOnFooTxt`, мы начинаем с проверки готовности `bar_txt_future`. Если она ещё не готова, мы выходим из цикла и возвращаем `Poll::Pending`. В противном случае, мы можем выполнить последнюю операцию функции `example`: конкатенацию переменной `content` с результатом футуры. Обновляем машину состояний в состояние `End` и затем возвращаем результат обёрнутый в `Poll::Ready`. +Аналогично состоянию `WaitingOnFooTxt`, мы начинаем с проверки готовности `bar_txt_future`. Если она ещё не готова, мы выходим из цикла и возвращаем `Poll::Pending`. В противном случае, мы можем выполнить последнюю операцию функции `example`: конкатенацию переменной `content` с результатом футуры. Переводим автомат в состояние `End` и затем возвращаем результат обёрнутый в `Poll::Ready`. В итоге, код для `End` состояния выглядит так: + ```rust ExampleStateMachine::End(_) => { panic!("poll вызван после возврата Poll::Ready"); } ``` -Футуры не должны повторно проверяться после того, как они вернули `Poll::Ready`, поэтому паникуем, если вызвана функция `poll`, когда мы уже находимся в состоянии `End`. -Теперь мы знаем, что сгенерированная машина состояний и ее реализация интерфейса `Future` _могла бы_ выглядеть так. На практике компилятор генерирует код по-другому. (Если вас заинтересует, то реализация ныне основана на [_корутинах_], но это только деталь имплементации.) +Футуры не должны повторно проверяться после того, как они вернули `Poll::Ready`, поэтому паникуем, если вызвана функция `poll` при состоянии `End`. + +Теперь мы знаем, что сгенерированная машина состояний и ее реализация интерфейса `Future` _могла бы_ выглядеть так. На практике компилятор генерирует код по-другому. (Если вас интересно, то реализация ныне основана на [_корутинах_], но это только деталь имплементации.) [_корутинах_]: https://doc.rust-lang.org/stable/unstable-book/language-features/coroutines.html -Последняя часть загадки – сгенерированный код для самой функции `example`. Помните, что заголовок функции был определён следующим образом: +Последняя часть пазла – сгенерированный код для самой функции `example`. Помните, что заголовок функции был определён следующим образом: ```rust async fn example(min_len: usize) -> String ``` -Теперь, когда весь функционал реализуется машиной состояний, единственное, что ф-ция должна сделать - это инициализировать эту машику и вернуть ее. Сгенерированный код для этого может выглядеть следующим образом: +Теперь, когда весь функционал реализуется конечным автоматом, единственное, что ф-ция должна сделать - это инициализировать этот автомат и вернуть его. Сгенерированный код для может выглядеть так: ```rust fn example(min_len: usize) -> ExampleStateMachine { @@ -489,20 +499,17 @@ fn example(min_len: usize) -> ExampleStateMachine { } ``` -Функция больше не имеет модификатора `async`, поскольку теперь явно возвращает тип `ExampleStateMachine`, который реализует трейт `Future`. Как ожидалось, машина состояний создается в состоянии `start` и соответствующая ему структура состояния инициализируется параметром `min_len`. +Функция больше не имеет модификатора `async`, поскольку теперь явно возвращает тип `ExampleStateMachine`, который реализует трейт `Future`. Как ожидалось, автомат создается в состоянии `start` и соответствующая ему структура состояния инициализируется параметром `min_len`. -Заметьте, что эта функция не запускает выполнение машины состояний. Это фундаментальное архитектурное решение для футур в Rust: они ничего не делают, пока не будет произведена первая проверка на готовность. +Заметьте, что эта функция не запускает автомат. Это фундаментальное архитектурное решение для футур в Rust: они ничего не делают, пока не будет произведена первая проверка на готовность. -#### Pinning -> [!note] Закрепление (pinning, пиннинг) +#### Закрепление Мы уже несколько раз столкнулись с понятием _закрепления_ (pinnig, пиннинг) в этом посте. Наконец, время чтобы изучить, что такое закрепление и почему оно необходимо. -> [!note] pinning - механизм, который гарантирует, что объект в памяти не будет перемещен. - #### Самоссылающиеся структуры -Как объяснялось выше, переходы конечных автоматов хранят локальные переменные для каждой точки остановки в структуре. Для простых примеров, как наш `example` функции, это было просто и не привело к никаким проблемам. Однако делаются сложнее, когда переменные ссылаются друг на друга. Например, рассмотрите следующую функцию: +Как объяснялось выше, переходы конечных автоматов хранят локальные переменные для каждой точки остановки в структуре. Для простых примеров, как наш `example` функции, это было просто и не привело к никаким проблемам. Однако делаются сложнее, когда переменные ссылаются друг на друга. Например, рассмотрим код: ```rust async fn pin_example() -> i32 { @@ -542,18 +549,19 @@ struct WaitingOnWriteState { Существует три основных подхода к решению проблемы висящих указателей (dangling pointers): -- **Обновление указателя при перемещении**: Идея состоит в обновлении внутреннего указателя при каждом перемещении структуры в памяти, чтобы она оставалась действительной после перемещения. Однако этот подход требует значительных изменений в Rust, которые могут привести к потенциальным значительным потерям производительности. Причина заключается в том, что необходимо каким-то образом отслеживать тип всех полей структуры и проверять на каждом операции перемещения, требуется ли обновление указателя. -- **Хранение смещения (offset) вместо самоссылающихся ссылок**: Чтобы избежать необходимости обновления указателей, компилятор мог бы попытаться хранить самоссы ссылки в форме смещений от начала структуры вместо прямых ссылок. Например, поле `element` вышеупомянутой `WaitingOnWriteState` структуры можно было бы хранить в виде поля `element_offset` c значением 8, потому что элемент массива, на который указывает ссылка, находится за 8 байтов после начала структуры. Смещение остается неизменным при перемещении структуры, так что не требуются обновления полей. - Проблема с этим подходом в том, что требуется, чтобы компилятор обнаружил всех самоссылок. Это невозможно на этапе компилящии потому, что значение ссылки может зависеть от ввода пользователя, так что нам потребуется система анализа ссылок и корректная генерация состояния для структур во время исполнения. Это приведёт к дополнительным расходам времени на выполнение, а также предотвратит определённые оптимизации компилятора, что приведёт к еще большим потерям производительности. -- **Запретить перемещать структуру**: Мы увидели выше, что висящий указатель возникает только при перемещении структуры в памяти. Запретив все операции перемещения для самоссылающихся структур, можно избежать этой проблемы. Большое преимущество этого подхода состоит в том, что он можно реализовать на уровне системы типов без дополнительных расходов времени выполнения. Недостаток заключается в том, что оно возлагает на программиста обязанности по обработке перемещений самоссылающихся структур. +- **Обновление указателя при перемещении**: Суть в обновлении внутреннего указателя при каждом перемещении структуры в памяти, чтобы она оставалась действительной после перемещения. Однако этот подход требует значительных изменений в Rust, которые могут привести к потенциальным значительным потерям производительности. Причина заключается в том, что необходимо каким-то образом отслеживать тип всех полей структуры и проверять на каждой операции перемещения, требуется ли обновление указателя. +- **Хранение смещения (offset) вместо самоссылающихся ссылок**: Чтобы избежать необходимости обновления указателей, компилятор мог бы попытаться хранить саммоссылки в форме смещений от начала структуры вместо прямых ссылок. Например, поле `element` вышеупомянутой `WaitingOnWriteState` структуры можно было бы хранить в виде поля `element_offset` c значением 8, потому что элемент массива, на который указывает ссылка, находится за 8 байтов после начала структуры. Смещение остается неизменным при перемещении структуры, так что не требуются обновления полей. -Rust выбрал третий подход из-за принципа предоставления _бесплатных абстракций_ (zero cost abstractions), что означает, что абстракции не должны накладывать дополнительные расходы времени выполнения. API [_pinning_] предлагалось для решения этой проблемы в RFC 2349 (). В следующем разделе мы дадим краткий обзор этого API и объясним, как оно работает с async/await и futures. + Проблема с этим подходом в том, что он требует от компилятора обнаружения всех самоссылок. Это невозможно на этапе компилящии, т.к. значения ссылки может зависеть от ввода пользователя, так что нам потребуется система анализа ссылок и корректная генерация состояния для структур во время исполнения. Это накладывает расходы на время выполнения и предотвратит определённые оптимизации компилятора, что приведёт к еще большим потерям производительности. +- **Запретить перемещать структуру**: Мы увидели выше, что висящий указатель возникает только при перемещении структуры в памяти. Запретив все операции перемещения для самоссылающихся структур, можно избежать этой проблемы. Большое преимущество том, что это можно реализовать на уровне системы типов без расходов к времени исполнения. Недостаток в том, что оно возлагает на программиста обязанности по обработке перемещений самоссылающихся структур. -#### Значения на Куче (Heap) +Rust выбрал третий подход из-за принципа предоставления _бесплатных абстракций_ (zero-cost abstractions), что означает, что абстракции не должны накладывать дополнительные расходы времени выполнения. API [_pinning_] предлагалось для решения этой проблемы в RFC 2349 (). В следующем разделе мы дадим краткий обзор этого API и объясним, как оно работает с async/await и futures. -Первый наблюдение состоит в том, что значения, выделенные на [куче], обычно имеют фиксированный адрес памяти. Они создаются с помощью вызова `allocate` и затем ссылаются на тип указателя, такой как `Box`. Хотя перемещение указательного типа возможно, значение кучи, которое указывает на него, остается в том же адресе памяти до тех пор, пока оно не будет освобождено с помощью вызова `deallocate` еще раз. +#### Значения в Куче (Heap) -[heap-allocated]: @/edition-2/posts/10-heap-allocation/index.md +Первый наблюдение состоит в том, что значения [аллоцированные в куче], обычно имеют фиксированный адрес памяти. Они создаются с помощью вызова `allocate` и затем ссылаются на тип указателя, такой как `Box`. Хотя перемещение указательного типа возможно, значение кучи, которое указывает на него, остается в том же адресе памяти до тех пор, пока оно не будет освобождено с помощью вызова `deallocate` еще раз. + +[аллоцированные в куче]: @/edition-2/posts/10-heap-allocation/index.md Используя аллокацию на куче, можно попытаться создать самоссылающуюся структуру: @@ -573,11 +581,11 @@ struct SelfReferential { } ``` -([Попробовать в песочнице][playground-self-ref]) +([Попробовать][playground-self-ref]) [playground-self-ref]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ce1aff3a37fcc1c8188eeaf0f39c97e8 -Мы создаем простую структуру с названием `SelfReferential`, которая содержит только одно поле c указателем. Во-первых, мы инициализируем эту структуру с пустым указателем и затем выделяем ее на куче с помощью `Box::new`. Затем мы определяем адрес кучи для выделенной структуры и храним его в переменной `ptr`. В конце концов, мы делаем структуру самоссылающейся, назначив переменную `ptr` полю `self_ptr`. +Мы создаем простую структуру с названием `SelfReferential`, которая содержит только одно поле c указателем. Во-первых, мы инициализируем эту структуру с пустым указателем и затем выделяем место в куче с помощью `Box::new`. Затем мы определяем адрес кучи для выделенной структуры и храним его в переменной `ptr`. В конце концов, мы делаем структуру самоссылающейся, назначив переменную `ptr` полю `self_ptr`. Когда мы запускаем этот код в [песочнице][playground-self-ref], мы видим, что адрес на куче и внутренний указатель равны, что означает, что поле `self_ptr` валидное. Поскольку переменная `heap_value` является только указателем, перемещение его (например, передачей в функцию) не изменяет адрес самой структуры, поэтому `self_ptr` остается действительным даже при перемещении указателя. @@ -591,19 +599,19 @@ println!("value at: {:p}", &stack_value); println!("internal reference: {:p}", stack_value.self_ptr); ``` -([Попробовать в песочнице](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e160ee8a64cba4cebc1c0473dcecb7c8)) +([Попробовать](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e160ee8a64cba4cebc1c0473dcecb7c8)) Мы используем функцию [`mem::replace`], чтобы заменить значение, выделенное в куче, новым экземпляром структуры. Это позволяет нам переместить исходное значение `heap_value` в стек, в то время как поле `self_ptr` структуры теперь является висящим указателем, который по-прежнему указывает на старый адрес в куче. Когда вы запустите пример в песочнице, вы увидите, что строки _«value at:»_ и _«internal reference:»_, показывают разные указатели. Таким образом, выделение значения в куче недостаточно для обеспечения безопасности самоссылок. [`mem::replace`]: https://doc.rust-lang.org/nightly/core/mem/fn.replace.html -Основная проблема, которая привела к вышеуказанной ошибке, заключается в том, что `Box` позволяет нам получить ссылку `&mut T` на значение, выделенное в куче. Эта ссылка `&mut` позволяет использовать такие методы, как [`mem::replace`] или [`mem::swap`], для аннулирования значения, выделенного в куче. Чтобы решить эту проблему, мы должны предотвратить создание ссылок `&mut` на самореференциальные структуры. +Основная проблема, которая привела к вышеуказанной ошибке, заключается в том, что `Box` позволяет нам получить ссылку `&mut T` на значение, выделенное в куче. Эта ссылка `&mut` позволяет использовать такие методы, как [`mem::replace`] или [`mem::swap`], для аннулирования значения, выделенного в куче. Чтобы решить эту проблему, мы должны предотвратить создание ссылок `&mut` на самоссылающиеся структуры. [`mem::swap`]: https://doc.rust-lang.org/nightly/core/mem/fn.swap.html #### `Pin>` и `Unpin` -API _закрепления_ предоставляет решение проблемы `&mut T` в виде типа-обертки [`Pin`] и трейта-маркера [`Unpin`]. Идея использования - ограничить все методы `Pin`, которые могут быть использованы для получения ссылок `&mut` на обернутое значение (например, [`get_mut`][pin-get-mut] или [`deref_mut`][pin-deref-mut]), на трейт `Unpin`. Трейт `Unpin` является _авто трейтом_ ([_auto trait_]), который автоматически реализуется для всех типов, за исключением тех, которые явно отказываются от него. Заставляя самореференциальные структуры отказаться от `Unpin`, не остается (безопасного) способа получить `&mut T` из типа `Pin>` для них. В результате их внутренние самореференции гарантированно остаются действительными. +API _закрепления_ предоставляет решение проблемы `&mut T` в виде типа-обертки [`Pin`] и трейта-маркера [`Unpin`]. Идея использования - ограничить все методы `Pin`, которые могут быть использованы для получения ссылок `&mut` на обернутое значение (например, [`get_mut`][pin-get-mut] или [`deref_mut`][pin-deref-mut]), на трейт `Unpin`. Трейт `Unpin` является _авто трейтом_ ([_auto trait_]), который автоматически реализуется для всех типов, за исключением тех, которые явно отказываются от него. Заставляя самоссылающиеся структуры отказаться от `Unpin`, не остается (безопасного) способа получить `&mut T` из типа `Pin>` для них. В результате их внутренние самоссылки гарантированно остаются действительными. [`Pin`]: https://doc.rust-lang.org/stable/core/pin/struct.Pin.html [`Unpin`]: https://doc.rust-lang.org/nightly/std/marker/trait.Unpin.html @@ -682,11 +690,11 @@ unsafe { Теперь единственной оставшейся ошибкой является желаемая ошибка на `mem::replace`. Помните, что эта операция пытается переместить значение, размещённое в куче, на стек, что нарушило бы самоссылку, хранящуюся в поле `self_ptr`. Отказываясь от `Unpin` и используя `Pin>`, мы можем предотвратить эту операцию на этапе компиляции и таким образом безопасно работать с самоссыльными структурами. Как мы видели, компилятор не может доказать, что создание самоссылки безопасно (пока), поэтому нам нужно использовать небезопасный блок и самостоятельно проверить корректность. -#### Пиннинг на стеке и `Pin<&mut T>` +#### Закрепление в стеке и `Pin<&mut T>` -В предыдущем разделе мы узнали, как использовать `Pin>` для безопасного создания самоссыльного значения, размещённого в куче. Хотя этот подход работает хорошо и относительно безопасен (кроме unsafe), необходимая аллокация в куче бьет по производительности. Поскольку Rust стремится предоставлять _абстракции с нулевыми затратами_ (_zero-cost abstractions_) где это возможно, API закрепления также позволяет создавать экземпляры `Pin<&mut T>`, которые указывают на значения, размещённые на стеке. +В предыдущем разделе мы узнали, как использовать `Pin>` для безопасного создания самоссыльного значения, размещённого в куче. Хотя этот подход работает хорошо и относительно безопасен (кроме unsafe), необходимая аллокация в куче бьет по производительности. Поскольку Rust стремится предоставлять _абстракции с нулевыми затратами_ (_zero-cost abstractions_) где это возможно, API закрепления также позволяет создавать экземпляры `Pin<&mut T>`, которые указывают на значения, размещённые в стеке. -В отличие от экземпляров `Pin>`, которые имеют _владение_ обёрнутым значением, экземпляры `Pin<&mut T>` лишь временно заимствуют обёрнутое значение. Это усложняет задачу, так как программисту необходимо самостоятельно обеспечивать дополнительные гарантии. Важно, чтобы `Pin<&mut T>` оставался закрепленным на протяжении всей жизни ссылочного `T`, что может быть сложно проверить для переменных на стеке. Чтобы помочь с этим, существуют такие крейты, как [`pin-utils`], но я все же не рекомендую закреплять на стеке, если вы не уверены в своих действиях. +В отличие от экземпляров `Pin>`, которые _владеют_ (ownership) обёрнутым значением, экземпляры `Pin<&mut T>` лишь временно заимствуют (borrow) обёрнутое значение. Это усложняет задачу, так как программисту необходимо самостоятельно обеспечивать дополнительные гарантии. Важно, чтобы `Pin<&mut T>` оставался закрепленным на протяжении всей жизни ссылочного `T`, что может быть сложно проверить для переменных на стеке. Чтобы помочь с этим, существуют такие крейты, как [`pin-utils`], но я все же не рекомендую закреплять на стеке, если вы не уверены в своих действиях. [`pin-utils`]: https://docs.rs/pin-utils/0.1.0-alpha.4/pin_utils/ @@ -695,7 +703,7 @@ unsafe { [`pin` module]: https://doc.rust-lang.org/nightly/core/pin/index.html [`Pin::new_unchecked`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.new_unchecked -#### Пиннинг и Футуры +#### Закрепление и Футуры Как мы уже увидели в этом посте, метод [`Future::poll`] использует пиннинг в виде параметра `Pin<&mut Self>`: @@ -709,7 +717,7 @@ fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll [self-ref-async-await]: @/edition-2/posts/12-async-await/index.md#self-referential-structs -Стоит отметить, что перемещение футур до первого вызова `poll` допустимо. Это связано с тем, что футуры являются ленивыми и ничего не делают, пока их не вызовут в первый раз. Состояние `start` сгенерированных конечных автоматов, следовательно, содержит только аргументы функции, но не внутренние ссылки. Чтобы вызвать `poll`, вызывающему необходимо сначала обернуть фьючерс в `Pin`, что гарантирует, что фьючерс больше не может быть перемещён в памяти. Поскольку пиннинг на стеке сложнее сделать правильно, я рекомендую всегда использовать [`Box::pin`] в сочетании с [`Pin::as_mut`] для этого. +Стоит отметить, что перемещение футур до первого вызова `poll` допустимо. Как упоминалось выше, футуры ленивые и ничего не делают, пока их не вызовут в первый раз. Состояние `start` сгенерированных автоматов, следовательно, содержит только аргументы функции, но не внутренние ссылки. Чтобы вызвать `poll`, вызывающему необходимо сначала обернуть футуру в `Pin`, что гарантирует, что футура больше не может быть перемещена в памяти. Поскольку пиннинг на стеке сложнее сделать правильно, я рекомендую всегда использовать [`Box::pin`] в сочетании с [`Pin::as_mut`] для этого. [`futures`]: https://docs.rs/futures/0.3.4/futures/ @@ -718,25 +726,23 @@ fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll [map-src]: https://docs.rs/futures-util/0.3.4/src/futures_util/future/future/map.rs.html [projections and structural pinning]: https://doc.rust-lang.org/stable/std/pin/index.html#projections-and-structural-pinning -### Executors and Wakers +### Исполнитель and Wakers Используя async/await, можно эргономично работать с футурами в полностью асинхронном режиме. Однако, как мы узнали выше, футуры ничего не делают, пока их не вызовут. Это означает, что нам нужно в какой-то момент вызвать `poll`, иначе асинхронный код никогда не будет выполнен. -Запуская одну футуры, мы можем вручную ожидать ее исполнения в цикле, [как описано выше](#waiting-on-futures). Однако этот подход очень неэффективен и непрактичен для программ, создающих большое количество футур. Наиболее распространённым решением этой проблемы является определение глобального _исполнителя_, который отвечает за опрос всех футур в системе, пока они не завершатся. +Запуская одну футуры, мы можем вручную ожидать ее исполнения в цикле, [как описано выше](#ожидание-futures). Однако этот подход очень неэффективен и непрактичен для программ, создающих большое количество футур. Наиболее распространённым решением этого является создание глобального _исполнителя_ (executor), который отвечает за опрос (polling) всех футур в системе, пока они не завершатся. -#### Executors +#### Исполнитель Цель исполнителя в том, чтобы позволить создавать футуры в качестве независимых задач, обычно через какой-либо метод `spawn`. Исполнитель затем отвечает за опрос всех футур, пока они не завершатся. Большое преимущество управления всеми футурами в одном месте состоит в том, что исполнитель может переключаться на другую футуру, когда текущая футура возвращает `Poll::Pending`. Таким образом, асинхронные операции выполняются параллельно, и процессор остаётся загруженным. -Многие реализации исполнителей также могут использовать преимущества систем с несколькими ядрами процессора. Они создают [thread pool], способный использовать все ядра, если достаточно работы, и применяют такие техники, как [work stealing], для балансировки нагрузки между ядрами. Существуют также специальные реализации исполнителей для встроенных систем, которые оптимизируют низкую задержку и затраты памяти. +Многие реализации исполнителей также могут использовать преимущества систем с мультиядерными процессорами. Они создают [пул потоков][thread pool], способный использовать все ядра, если достаточно работы, и применяют такие техники, как [work stealing], для балансировки нагрузки между ядрами. Существуют также специальные реализации исполнителей для встроенных систем, которые оптимизируют низкую задержку и затраты памяти. [thread pool]: https://en.wikipedia.org/wiki/Thread_pool [work stealing]: https://en.wikipedia.org/wiki/Work_stealing Чтобы избежать накладных расходов на повторный опрос футур, исполнители обычно используют API _waker_, поддерживаемый футурами Rust. - - #### Wakers Идея API waker в том, что специальный тип [`Waker`] передаётся в каждом вызове `poll`, при этом обернутый в тип [`Context`]. Этот тип `Waker` создаётся исполнителем и может использоваться асинхронной задачей для сигнализации о своём (частичном) завершении. В результате исполнитель не должен вызывать `poll` на футуре, которая ранее вернула `Poll::Pending`, пока не получит уведомление от соответствующего waker. @@ -755,30 +761,30 @@ async fn write_file() { Мы увидим, как работает тип `Waker` в деталях, когда создадим свой собственный исполнитель с поддержкой waker в разделе реализации этого поста. -### Cooperative Multitasking? +### Кооперативная Мультизадачность? -В начале этого поста мы говорили о вытесняющей (preemptive) и кооперативной многозадачности. В то время как вытесняющая многозадачность полагается на операционную систему для принудительного переключения между выполняемыми задачами, кооперативная многозадачность требует, чтобы задачи добровольно уступали контроль над CPU через операцию _yield_ на регулярной основе. Большое преимущество кооперативного подхода в том, что задачи могут сохранять своё состояние самостоятельно, что приводит к более эффективным переключениям контекста и делает возможным совместное использование одного и того же стека вызовов между задачами. +В начале этого поста мы говорили о вытесняющей (preemptive) и кооперативной (cooperative) многозадачности. В то время как вытесняющая многозадачность полагается на операционную систему для принудительного переключения между выполняемыми задачами, кооперативная многозадачность требует, чтобы задачи добровольно уступали контроль над CPU через операцию _yield_ на регулярной основе. Большое преимущество кооперативного подхода в том, что задачи могут сохранять своё состояние самостоятельно, что приводит к более эффективным переключениям контекста и делает возможным совместное использование одного и того же стека вызовов между задачами. Это может не быть сразу очевидным, но футуры и async/await представляют собой реализацию кооперативного паттерна многозадачности: -- Каждая футура, добавляемая в исполнитель, по сути является кооперативной задачей. +- Каждая футура, добавленная в исполнитель, по сути является кооперативной задачей. - Вместо использования явной операции yield, футуры уступают контроль над ядром CPU, возвращая `Poll::Pending` (или `Poll::Ready` в конце). - - Нет ничего, что заставляло бы футуру уступать CPU. Если они захотят, они могут никогда не возвращаться из `poll`, например, бесконечно выполняя цикл. + - Нет ничего, что заставляло бы футуру уступать ЦПУ. Если они захотят, они могут никогда не возвращать ответ на `poll`, например, бесконечно выполняя цикл. - Поскольку каждая футура может блокировать выполнение других футур в исполнителе, нам нужно доверять им, чтобы они не были вредоносными (malicious). -- Футуры внутренне хранят всё состояние, необходимое для продолжения выполнения при следующем вызове `poll`. При использовании async/await компилятор автоматически определяет все переменные, которые необходимы, и сохраняет их внутри сгенерированной машины состояний. +- Футуры хранят все состояние внутри, которое необходимо для продолжения выполнения при следующем вызове `poll`. При использовании async/await компилятор автоматически определяет все переменные, которые необходимы, и сохраняет их внутри сгенерированной машины состояний. - Сохраняется только минимально необходимое состояние для продолжения. - Поскольку метод `poll` отдает стек вызовов при возврате, тот же стек может использоваться для опроса других футур. Мы видим, что футуры и async/await идеально соответствуют паттерну кооперативной многозадачности; они просто используют другую терминологию. В дальнейшем мы будем использовать термины "задача" и "футура" взаимозаменяемо. -## Implementation +## Реализация Теперь, когда мы понимаем, как работает кооперативная многозадачность на основе футур и async/await в Rust, пора добавить поддержку этого в наш ядро. Поскольку трейт [`Future`] является частью библиотеки `core`, а async/await — это особенность самого языка, нам не нужно делать ничего особенного, чтобы использовать его в нашем `#![no_std]` ядре. Единственное требование — использовать, как минимум, nightly версию Rust от `2020-03-25`, поскольку до этого времени async/await не поддерживала `no_std`. С достаточно свежей nightly версией мы можем начать использовать async/await в нашем `main.rs`: ```rust -// in src/main.rs +// src/main.rs async fn async_number() -> u32 { 42 @@ -790,11 +796,11 @@ async fn example_task() { } ``` -Функция `async_number` является `async fn`, поэтому компилятор преобразует её в машину состояний, реализующую `Future`. Поскольку функция возвращает только `42`, результирующая футура непосредственно вернёт `Poll::Ready(42)` при первом вызове `poll`. Как и `async_number`, функция `example_task` также является `async fn`. Она ожидает число, возвращаемое `async_number`, а затем выводит его с помощью макроса `println`. +Здесь `async_number` является `async fn`, поэтому компилятор преобразует её в машину состояний, реализующую `Future`. Поскольку функция возвращает только `42`, результирующая футура непосредственно вернёт `Poll::Ready(42)` при первом вызове `poll`. Как и `async_number`, функция `example_task` также является `async fn`. Она ожидает число, возвращаемое `async_number`, а затем выводит его с помощью макроса `println`. Чтобы запустить футуру, которую вернул `example_task`, нам нужно вызывать `poll` на ней, пока он не сигнализирует о своём завершении, возвращая `Poll::Ready`. Для этого нам нужно создать простой тип исполнителя. -### Task +### Задачи Перед тем как начать реализацию исполнителя, мы создаем новый модуль `task` с типом `Task`: @@ -815,17 +821,17 @@ pub struct Task { } ``` -Структура `Task` является обёрткой вокруг _закрепленной_, _размещённой в куче_ и _динамически диспетчеризуемой футуры_ с пустым типом `()` в качестве выходного значения. Давайте разберём её подробнее: +Структура `Task` является обёрткой вокруг _закрепленной_, _размещённой в куче_ и _динамически диспетчеризуемой футуры_ с пустым типом `()` в качестве выходного значения. Разберём её подробнее: - Мы требуем, чтобы футура, связанная с задачей, возвращала `()`. Это означает, что задачи не возвращают никаких результатов, они просто выполняются для побочных эффектов. Например, функция `example_task`, которую мы определили выше, не имеет возвращаемого значения, но выводит что-то на экран как побочный эффект (side effect). - Ключевое слово `dyn` указывает на то, что мы храним [_trait object_] в `Box`. Это означает, что методы на футуре диспетчеризуются динамически, позволяя хранить в типе `Task` разные типы футур. Это важно, поскольку каждая `async fn` имеет свой собственный тип, и мы хотим иметь возможность создавать несколько разных задач. -- Как мы узнали в [разделе о закреплении], тип `Pin` обеспечивает, что значение не может быть перемещено в памяти, помещая его в кучу и предотвращая создание `&mut` ссылок на него. Это важно, потому что фьючерсы, генерируемые async/await, могут быть самоссыльными, т.е. содержать указатели на себя, которые станут недействительными, если футура будет перемещена. +- Как мы узнали в [разделе о закреплении], тип `Pin` обеспечивает, что значение не может быть перемещено в памяти, помещая его в кучу и предотвращая создание `&mut` ссылок на него. Это важно, потому что футуры, генерируемые async/await, могут быть самоссылающимися, т.е. содержать указатели на себя, которые станут недействительными, если футура будет перемещена. [_trait object_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html [_dynamically dispatched_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html#trait-objects-perform-dynamic-dispatch -[разделе о закреплении]: #pinning +[разделе о закреплении]: #Закрепление -Чтобы разрешить создание новых структур `Task` из фьючерсов, мы создаём функцию `new`: +Чтобы разрешить создание новых структур `Task` из футур, мы создаём функцию `new`: ```rust // in src/task/mod.rs @@ -859,18 +865,18 @@ impl Task { Поскольку метод [`poll`] трейта `Future` ожидает вызова на типе `Pin<&mut T>`, мы сначала используем метод [`Pin::as_mut`], чтобы преобразовать поле `self.future` типа `Pin>`. Затем мы вызываем `poll` на преобразованном поле `self.future` и возвращаем результат. Поскольку метод `Task::poll` должен вызываться только исполнителем, который мы создадим через мгновение, мы оставляем функцию приватной для модуля `task`. -### Simple Executor +### Простой Исполнитель Поскольку исполнители могут быть довольно сложными, мы намеренно начинаем с создания очень базового исполнителя, прежде чем реализовывать более продвинутого. Для этого мы сначала создаём новый подмодуль `task::simple_executor`: ```rust -// in src/task/mod.rs +// src/task/mod.rs pub mod simple_executor; ``` ```rust -// in src/task/simple_executor.rs +// src/task/simple_executor.rs use super::Task; use alloc::collections::VecDeque; @@ -895,7 +901,7 @@ impl SimpleExecutor { Структура содержит единственное поле `task_queue` типа [`VecDeque`], которое по сути является вектором, позволяющим выполнять операции добавления и удаления с обоих концов. Идея в том, что мы можем вставлять новые задачи через метод `spawn` в конец и извлекаем следующую задачу для выполнения из начала. Таким образом, мы получаем простую [FIFO очередь] ("первый пришёл — первый вышел"). [`VecDeque`]: https://doc.rust-lang.org/stable/alloc/collections/vec_deque/struct.VecDeque.html -[FIFO очередь]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics) +[FIFO очередь]: https://ru.wikipedia.org/wiki/FIFO #### Dummy Waker @@ -935,12 +941,12 @@ fn dummy_waker() -> Waker { [`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html [`Box::into_raw`]: https://doc.rust-lang.org/stable/alloc/boxed/struct.Box.html#method.into_raw -##### A Dummy `RawWaker` +##### Заклушка `RawWaker` Хотя вручную создавать `RawWaker` не рекомендуется, в настоящее время нет другого способа создать заглушку `Waker`, которая ничего не делает. К счастью, тот факт, что мы хотим ничего не делать, делает реализацию функции `dummy_raw_waker` относительно безопасной: ```rust -// in src/task/simple_executor.rs +// src/task/simple_executor.rs use core::task::RawWakerVTable; @@ -959,12 +965,12 @@ fn dummy_raw_waker() -> RawWaker { После создания `vtable` мы используем функцию [`RawWaker::new`] для создания `RawWaker`. Переданный `*const ()` не имеет значения, поскольку ни одна из функций vtable не использует его. По этой причине мы просто передаем нулевой указатель. -#### A `run` Method +#### Метод `run` -Теперь у нас есть способ создать экземпляр `Waker`, и мы можем использовать его для реализации метода `run` в нашем исполнителе. Самый простой метод `run` — это многократный опрос всех задач в очереди в цикле до тех пор, пока все они не будут выполнены. Это не очень эффективно, так как не использует уведомления от `Waker`, но это простой способ запустить эти штуки: +Теперь у нас есть способ создать экземпляр `Waker`, и мы можем использовать его для реализации метода `run` в нашем исполнителе. Самый простой метод `run` — это многократный опрос всех задач в очереди в цикле до тех пор, пока все они не будут выполнены. Это не очень эффективно, так как не использует уведомления от `Waker`, но это простой способ запустить это: ```rust -// in src/task/simple_executor.rs +// src/task/simple_executor.rs use core::task::{Context, Poll}; @@ -974,7 +980,7 @@ impl SimpleExecutor { let waker = dummy_waker(); let mut context = Context::from_waker(&waker); match task.poll(&mut context) { - Poll::Ready(()) => {} // task готов + Poll::Ready(()) => {} // задача выполнена Poll::Pending => self.task_queue.push_back(task), } } @@ -984,12 +990,12 @@ impl SimpleExecutor { Функция использует цикл `while let`, чтобы обработать все задачи в `task_queue`. Для каждой задачи сначала создаётся тип `Context`, оборачивая экземпляр `Waker`, возвращаемый нашей функцией `dummy_waker`. Затем вызывается метод `Task::poll` с этим `context`. Если метод `poll` возвращает `Poll::Ready`, задача завершена, и мы можем продолжить с следующей задачей. Если задача всё ещё `Poll::Pending`, мы добавляем её в конец очереди, чтобы она была опрошена снова в следующей итерации цикла. -#### Trying It +#### Опробуем это С нашим типом `SimpleExecutor` мы теперь можем попробовать запустить задачу, возвращаемую функцией `example_task`, в нашем `main.rs`: ```rust -// in src/main.rs +// src/main.rs use blog_os::task::{Task, simple_executor::SimpleExecutor}; @@ -1040,9 +1046,9 @@ async fn example_task() { [_Interrupts_]: @/edition-2/posts/07-hardware-interrupts/index.md -В дальнейшем мы создадим асинхронную задачу на основе прерывания клавиатуры. Прерывание клавиатуры выбраны т.к. это хороший кандидат, т.к. это они недетерминированны, так и критично по времени задержки. Недетерминированность означает, что невозможно предсказать, когда произойдёт нажатие клавиши, поскольку это полностью зависит от пользователя. Критичность по времени задержки означает, что мы хотим обрабатывать ввод с клавиатуры своевременно, иначе пользователь почувствует задержку. Чтобы эффективно поддерживать такую задачу, исполнителю будет необходимо обеспечить надлежащую поддержку уведомлений `Waker`. +В дальнейшем мы создадим асинхронную задачу на основе прерывания клавиатуры. Это хороший кандидат, они недетерминированны и критичны по времени задержки. Недетерминированность означает, что невозможно предсказать, когда произойдёт нажатие клавиши, поскольку это полностью зависит от пользователя. Критичность ко времени задержки означает, что мы хотим обрабатывать ввод с клавиатуры своевременно, иначе пользователь почувствует задержку. Чтобы эффективно поддерживать такую задачу, исполнителю будет необходимо обеспечить надлежащую поддержку уведомлений `Waker`. -#### Scancode Queue +#### Очередь Скан-кодов Сейчас мы обрабатываем ввод с клавиатуры непосредственно в обработчике прерываний. Это нехорошая реализация в долгосрочной перспективе, потому что обработчики прерываний должны быть как можно короче ( ), так как они могут прерывать важную работу. Вместо этого обработчики прерываний должны выполнять только минимальный объем необходимой работы (например, считывание кода сканирования клавиатуры) и оставлять остальную работу (например, интерпретацию кода сканирования) фоновой задаче. From d2bb57a8097d15b83f88c5e64e24db32591b300f Mon Sep 17 00:00:00 2001 From: TakiMoysha <36836047+TakiMoysha@users.noreply.github.com> Date: Mon, 15 Sep 2025 20:36:15 +0200 Subject: [PATCH 6/9] completed translation of post 12 --- .../posts/12-async-await/index.ru.md | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/blog/content/edition-2/posts/12-async-await/index.ru.md b/blog/content/edition-2/posts/12-async-await/index.ru.md index b6061ac5..45519b02 100644 --- a/blog/content/edition-2/posts/12-async-await/index.ru.md +++ b/blog/content/edition-2/posts/12-async-await/index.ru.md @@ -1038,7 +1038,7 @@ async fn example_task() { - Т.к `example_task` напрямую возвращает `Poll::Ready`, она не добавляется обратно в очередь задач. - Метод `run` возвращается после того, как `task_queue` становится пустым. Выполнение нашей функции `kernel_main` продолжается, и выводится сообщение _"It did not crash!"_. -### Async Keyboard Input +### Асинхронный ввод с клавиатуры Наш простой исполнитель не использует уведомления `Waker` и просто циклически обрабатывает все задачи до тех пор, пока они не завершатся. Это не было проблемой для нашего примера, так как наш `example_task` может завершиться сразу при первом вызове `poll`. Чтобы увидеть преимущества производительности правильной реализации `Waker`, нам нужно сначала создать задачу, которая действительно асинхронна, т.е. задачу, которая, вероятно, вернёт `Poll::Pending` при первом вызове `poll`. @@ -1050,19 +1050,19 @@ async fn example_task() { #### Очередь Скан-кодов -Сейчас мы обрабатываем ввод с клавиатуры непосредственно в обработчике прерываний. Это нехорошая реализация в долгосрочной перспективе, потому что обработчики прерываний должны быть как можно короче ( ), так как они могут прерывать важную работу. Вместо этого обработчики прерываний должны выполнять только минимальный объем необходимой работы (например, считывание кода сканирования клавиатуры) и оставлять остальную работу (например, интерпретацию кода сканирования) фоновой задаче. +Сейчас мы обрабатываем ввод с клавиатуры непосредственно в обработчике прерываний. Это не лучший подход в долгосрочной перспективе, обработка прерываний должна выполняться как можно быстрее, так как они могут прерывать важную работу. Вместо этого обработчики прерываний должны выполнять только минимальный объем необходимой работы (например, считывание кода сканирования клавиатуры) и оставлять остальную работу (например, интерпретацию кода сканирования) фоновой задаче. -Распространённым шаблоном для делегирования работы фоновым задачам является очередь. Обработчик прерываний добавляет единицы работы в очередь, а фоновая задача обрабатывает работу в очереди. Применительно к нашему прерыванию клавиатуры это означает, что обработчик прерываний только считывает скан-код с клавиатуры, добавляет его в очередь, а затем возвращается. Задача клавиатуры находится на другом конце очереди и интерпретирует и обрабатывает каждый скан-код, который в неё добавляется: +Распространённым шаблоном для делегирования работы фоновым задачам является очередь. Обработчик прерываний добавляет единицы работы в очередь, а фоновая задача обрабатывает работу в очереди. Применительно к нашем прерываниям это означает, что обработчик прерываний только считывает скан-код с клавиатуры, добавляет его в очередь, а затем возвращается. Задача клавиатуры находится на другом конце очереди и интерпретирует и обрабатывает каждый скан-код, который в неё добавляется: ![Очередь скан-кодов с 8 слотами вверху. Обработчик прерываний клавиатуры внизу слева с стрелкой "добавить скан-код" слева от очереди. Задача клавиатуры внизу справа со стрелкой "извлечь скан-код", идущей с правой стороны очереди.](scancode-queue.svg) -Простая реализация такой очереди может быть основана на `VecDeque`, защищённом мьютексом. Однако использование мьютексов в обработчиках прерываний — не очень хорошая идея, так как это может легко привести к взаимным блокировкам (deadlock). Например, пользователь нажимает клавишу, но в тот же момент задача клавиатуру заблокировала очередь, обработчик прерываний пытается снова захватить блокировку и застревает навсегда. Ещё одна проблема с этим подходом в том, что `VecDeque` автоматически увеличивает свою ёмкость, выполняя новое выделение памяти в куче, когда она заполняется. Это также может привести к взаимным блокировкам, так как наш аллокатор также использует внутренний мьютекс. Другими проблемами являются то, что выделение памяти в куче может не удаться или занять значительное время, когда куча фрагментирована. +Простая реализация такой очереди может быть основана на `VecDeque`, защищённом мьютексом. Однако использование мьютексов в обработчиках прерываний — не очень хорошая идея, так как это может легко привести к взаимным блокировкам (deadlock). Например, пользователь нажимает клавишу, но в тот же момент задача клавиатуру заблокировала очередь, обработчик прерываний пытается снова захватить блокировку и застревает навсегда. Ещё одна проблема с этим подходом в том, что `VecDeque` автоматически увеличивает свою ёмкость, выполняя новое выделение памяти в куче, когда она заполняется. Это также может привести к взаимным блокировкам, так как наш аллокатор также использует внутренний мьютекс. Более того, выделение памяти в куче может не получиться или занять значительное время, если куча фрагментирована. -Чтобы предотвратить эти проблемы, нам нужна реализация очереди, которая не требует мьютексов или выделений памяти для своей операции `push`. Такие очереди могут быть реализованы с использованием неблокирующих [атомарных операций] для добавления и извлечения элементов. Таким образом, возможно создать операции `push` и `pop`, которые требуют только ссылки `&self` и могут использоваться без мьютекса. Чтобы избежать выделений памяти при `push`, очередь может быть основана на заранее выделенном буфере фиксированного размера. Хотя это делает очередь _ограниченной_ (_bounded_) (т.е. у неё есть максимальная длина), на практике часто возможно определить разумные верхние границы для длины очереди, так что это не представляет собой большой проблемы. +Чтобы предотвратить эти проблемы, нам нужна реализация очереди, которая не требует мьютексов или выделений памяти для своей операции `push`. Такие очереди могут быть реализованы с использованием неблокирующих [атомарных операций][atiomic operations] для добавления и извлечения элементов. Таким образом, возможно создать операции `push` и `pop`, которые требуют только ссылки `&self` и могут использоваться без мьютекса. Чтобы избежать выделений памяти при `push`, очередь может быть основана на заранее выделенном буфере фиксированного размера. Хотя это делает очередь _ограниченной_ (_bounded_) (т.е. у неё есть максимальная длина), на практике часто возможно определить разумные верхние границы для длины очереди, так что это не представляет собой большой проблемы. [atomic operations]: https://doc.rust-lang.org/core/sync/atomic/index.html -##### The `crossbeam` Crate +##### Крейт `crossbeam` Реализовать такую очередь правильно и эффективно очень сложно, поэтому я рекомендую придерживаться существующих, хорошо протестированных реализаций. Один из популярных проектов на Rust, который реализует различные типы без мьютексов для конкурентного программирования — это [`crossbeam`]. Он предоставляет тип под названием [`ArrayQueue`], который именно то, что нам нужно в данном случае. И нам повезло: этот тип полностью совместим с `no_std` библиотеками, поддерживающими выделение памяти. @@ -1072,7 +1072,7 @@ async fn example_task() { Чтобы использовать этот тип, нам нужно добавить зависимость на библиотеку `crossbeam-queue`: ```toml -# in Cargo.toml +# Cargo.toml [dependencies.crossbeam-queue] version = "0.3.11" @@ -1082,7 +1082,7 @@ features = ["alloc"] По умолчанию библиотека зависит от стандартной библиотеки. Чтобы сделать её совместимой с `no_std`, нам нужно отключить её стандартные функции и вместо этого включить функцию `alloc`. (Заметьте, что мы также могли бы добавить зависимость на основную библиотеку `crossbeam`, которая повторно экспортирует библиотеку `crossbeam-queue`, но это привело бы к большему количеству зависимостей и более длительному времени компиляции.) -##### Queue Implementation +##### Реализация Очереди Используя тип `ArrayQueue`, мы теперь можем создать глобальную очередь скан-кодов в новом модуле `task::keyboard`: @@ -1116,11 +1116,11 @@ version = "0.2.0" default-features = false ``` -Вместо примитива [`OnceCell`] мы также могли бы использовать макрос [`lazy_static`]. Однако тип `OnceCell` имеет то преимущество, что мы можем гарантировать, что инициализация не произойдёт в обработчике прерываний, тем самым предотвращая выполнение выделения памяти в куче в обработчике прерываний. +Вместо примитива [`OnceCell`] мы также могли бы использовать макрос [`lazy_static`]. Однако тип `OnceCell` имеет то преимущество, что мы можем гарантировать, что инициализация не произойдёт в обработчике прерываний, тем самым предотвращая выполнение аллокации в куче в обработчике прерываний. [`lazy_static`]: https://docs.rs/lazy_static/1.4.0/lazy_static/index.html -#### Filling the Queue +#### Наполнение очереди Чтобы заполнить очередь скан-кодов, мы создаём новую функцию `add_scancode`, которую будем вызывать из обработчика прерываний: @@ -1176,7 +1176,7 @@ extern "x86-interrupt" fn keyboard_interrupt_handler( Как и ожидалось, нажатия клавиш больше не выводятся на экран, когда мы запускаем наш проект с помощью `cargo run`. Вместо этого пишется предупреждение, что очередь не инициализирована при каждом нажатия клавиши. -#### Scancode Stream +#### Стрим для скан-кодов Чтобы инициализировать `SCANCODE_QUEUE` и считывать скан-коды из очереди асинхронным способом, мы создаём новый тип `ScancodeStream`: @@ -1198,11 +1198,11 @@ impl ScancodeStream { Цель поля `_private` — предотвратить создание структуры из внешних модулей. Это делает функцию `new` единственным способом создать данный тип. В функции мы сначала пытаемся инициализировать статическую переменную `SCANCODE_QUEUE`. Если она уже инициализирована, мы вызываем панику, чтобы гарантировать, что можно создать только один экземпляр `ScancodeStream`. -Чтобы сделать скан-коды доступными для асинхронных задач, далее нужно реализовать метод, подобный `poll`, который пытается извлечь следующий скан-код из очереди. Хотя это звучит так, будто мы должны реализовать трейт [`Future`] для нашего типа, здесь он не подходит. Проблема в том, что трейт `Future` абстрагируется только над одним асинхронным значением и ожидает, что метод `poll` не будет вызываться снова после того, как он вернёт `Poll::Ready`. Наша очередь скан-кодов, однако, содержит несколько асинхронных значений, поэтому нормально продолжать опрашивать её. +Чтобы сделать скан-коды доступными для асинхронных задач, нужно реализовать метод, подобный `poll`, который пытается извлечь следующий скан-код из очереди. Хотя это звучит так, будто мы должны реализовать трейт [`Future`] для нашего типа, здесь он не подходит. Проблема в том, что трейт `Future` абстрагируется только над одним асинхронным значением и ожидает, что метод `poll` не будет вызываться снова после того, как он вернёт `Poll::Ready`. Наша очередь скан-кодов, однако, содержит несколько асинхронных значений, поэтому нормально продолжать опрашивать её. -##### The `Stream` Trait +##### Трейт `Stream` -Поскольку типы, которые возвращают несколько асинхронных значений, являются распространёнными, библиотека [`futures`] предоставляет полезную абстракцию для таких типов: трейт [`Stream`]. Трейт определяется следующим образом: +Поскольку типы, которые возвращают несколько асинхронных значений, являются распространёнными, библиотека [`futures`] предоставляет полезную абстракцию для таких типов: трейт [`Stream`]. Определение трейта: [`Stream`]: https://rust-lang.github.io/async-book/05_streams/01_chapter.html @@ -1220,11 +1220,11 @@ pub trait Stream { - Ассоциированный тип называется `Item`, а не `Output`. - Вместо метода `poll`, который возвращает `Poll`, трейт `Stream` определяет метод `poll_next`, который возвращает `Poll>` (обратите внимание на дополнительный `Option`). -Существует также семантическое отличие: метод `poll_next` можно вызывать многократно, пока он не вернёт `Poll::Ready(None)`, чтобы сигнализировать о том, что поток завершён. В этом отношении метод похож на метод [`Iterator::next`], который также возвращает `None` после последнего значения. +Существует также семантическое отличие: метод `poll_next` можно вызывать многократно, пока он не вернёт `Poll::Ready(None)`, чтобы сигнализировать о том, что стрим завершён. В этом отношении метод похож на метод [`Iterator::next`], который также возвращает `None` после последнего значения. [`Iterator::next`]: https://doc.rust-lang.org/stable/core/iter/trait.Iterator.html#tymethod.next -##### Implementing `Stream` +##### Реализация `Stream` Давайте реализуем трейт `Stream` для нашего `ScancodeStream`, чтобы предоставлять значения из `SCANCODE_QUEUE` асинхронным способом. Для этого нам сначала нужно добавить зависимость на библиотеку `futures-util`, которая содержит тип `Stream`: @@ -1237,7 +1237,7 @@ default-features = false features = ["alloc"] ``` -Мы отключаем стандартные функции, чтобы сделать библиотеку совместимой с `no_std`, и включаем функцию `alloc`, чтобы сделать доступными её типы, основанные на выделении памяти (это понадобится позже). (Заметьте, что мы также могли бы добавить зависимость на основную библиотеку `futures`, которая повторно экспортирует библиотеку `futures-util`, но это привело бы к большему количеству зависимостей и более длительному времени компиляции.) +Мы отключаем стандартные функции, чтобы сделать библиотеку совместимой с `no_std`, и включаем функцию `alloc`, чтобы сделать доступными её типы, основанные на аллокации памяти (это понадобится позже). (Заметьте, что мы также могли бы добавить зависимость на основную библиотеку `futures`, которая повторно экспортирует библиотеку `futures-util`, но это привело бы к большему количеству зависимостей и более длительному времени компиляции.) Теперь мы можем импортировать и реализовать трейт `Stream`: @@ -1264,9 +1264,9 @@ impl Stream for ScancodeStream { [`ArrayQueue::pop`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.pop -#### Waker Support +#### Поддержка Waker -Как и метод `Futures::poll`, метод `Stream::poll_next` требует от асинхронной задачи уведомить исполнителя, когда она становится готовой после возврата `Poll::Pending`. Таким образом, исполнителю не нужно повторно опрашивать ту же задачу, пока она не будет уведомлена, что значительно снижает накладные расходы на ожидание задач. +Как и метод `Futures::poll`, метод `Stream::poll_next` требует от асинхронной задачи уведомить исполнителя, когда она становится готовой после возврата `Poll::Pending`. Таким образом, исполнителю не нужно повторно опрашивать ту же задачу, пока она не получит сигнал, что значительно снижает накладные расходы на ожидание задач. Чтобы отправить это уведомление, задача должна извлечь [`Waker`] из переданной ссылки [`Context`] и сохранить его где-то. Когда задача становится готовой, она должна вызвать метод [`wake`] на сохранённом `Waker`, чтобы уведомить исполнителя о том, что задачу следует опросить снова. @@ -1288,9 +1288,9 @@ static WAKER: AtomicWaker = AtomicWaker::new(); Идея в том, что реализация `poll_next` хранит текущий `waker` в этой статической переменной, а функция `add_scancode` вызывает функцию `wake` на ней, когда новый скан-код добавляется в очередь. -##### Storing a Waker +##### Хранение Waker -Контракт, определяемый `poll`/`poll_next`, требует, чтобы задача зарегистрировала уведомление для переданного `Waker`, когда она возвращает `Poll::Pending`. Давайте изменим нашу реализацию `poll_next`, чтобы удовлетворить этому требованию: +Контракт, определяемый `poll`/`poll_next`, требует, чтобы задача зарегистрировала уведомление для переданного `Waker`, когда она возвращает `Poll::Pending`. Давайте изменим нашу реализацию `poll_next`, чтобы соблюдать это требование: ```rust // src/task/keyboard.rs @@ -1331,7 +1331,7 @@ impl Stream for ScancodeStream { Обратите внимание, что существует два способа, которыми может произойти уведомление для задачи, которая ещё не вернула `Poll::Pending`. Один из способов — это упомянутое состояние гонки, когда уведомление происходит незадолго до возвращения `Poll::Pending`. Другой способ — это когда очередь больше не пуста после регистрации `waker`, так что возвращается `Poll::Ready`. Поскольку эти ложные уведомления предотвратить невозможно, исполнитель должен уметь правильно с ними справляться. -##### Waking the Stored Waker +##### Пробуждение хранящихся Waker Чтобы разбудить сохранённый `Waker`, мы добавляем вызов `WAKER.wake()` в функцию `add_scancode`: @@ -1351,13 +1351,13 @@ pub(crate) fn add_scancode(scancode: u8) { } ``` -Единственное изменение, которое мы внесли, — это добавление вызова `WAKER.wake()`, если добавление в очередь скан-кодов прошло успешно. Если в статической переменной `WAKER` зарегистрирован `waker`, этот метод вызовет одноимённый метод [`wake`] на нём, который уведомляет исполнителя. В противном случае операция не приводит к никаким действиям (no-op). +Единственное изменение, которое мы внесли, — это добавление вызова `WAKER.wake()`, если добавление в очередь скан-кодов прошло успешно. Если в статической переменной `WAKER` зарегистрирован `waker`, этот метод вызовет одноимённый метод [`wake`] на нём, который уведомляет исполнителя. Иначе операция ничего не делает. [`wake`]: https://doc.rust-lang.org/stable/core/task/struct.Waker.html#method.wake Важно, чтобы мы вызывали `wake` только после добавления в очередь, потому что в противном случае задача может быть разбужена слишком рано, пока очередь всё ещё пуста. Это может, например, произойти при использовании многопоточного исполнителя, который запускает пробуждённую задачу параллельно на другом ядре CPU. Хотя у нас пока нет поддержки потоков, мы добавим её скоро и не хотим, чтобы всё сломалось в этом случае. -#### Keyboard Task +#### Задачи Клавиатуры Теперь, когда мы реализовали трейт `Stream` для нашего `ScancodeStream`, мы можем использовать его для создания асинхронной задачи клавиатуры: @@ -1386,13 +1386,13 @@ pub async fn print_keypresses() { } ``` -Код очень похож на тот, который у нас был в нашем обработчике прерываний клавиатуры ([keyboard interrupt handler]) до того, как мы его изменили в этом посте. Единственное различие в том, что вместо чтения скан-кода из порта ввода-вывода мы берем его из `ScancodeStream`. Для этого мы сначала создаем новый поток `Scancode`, а затем многократно используем метод [`next`], предоставляемый трейтами [`StreamExt`], чтобы получить `Future`, который разрешается в следующий элемент потока. Используя оператор `await`, мы асинхронно ожидаем результат этого `Future`. +Код очень похож на тот, который у нас был в нашем обработчике прерываний клавиатуры ([keyboard interrupt handler]) до того, как мы его изменили в этом посте. Единственное различие в том, что вместо чтения скан-кода из порта ввода-вывода мы берем его из `ScancodeStream`. Для этого мы сначала создаем новый стрим `Scancode`, а затем многократно используем метод [`next`], предоставляемый трейтами [`StreamExt`], чтобы получить `Future`, который разрешается в следующий элемент стрима. Используя оператор `await`, мы асинхронно ожидаем результат этого `Future`. [keyboard interrupt handler]: @/edition-2/posts/07-hardware-interrupts/index.md#interpreting-the-scancodes [`next`]: https://docs.rs/futures-util/0.3.4/futures_util/stream/trait.StreamExt.html#method.next [`StreamExt`]: https://docs.rs/futures-util/0.3.4/futures_util/stream/trait.StreamExt.html -Мы используем `while let`, чтобы проходить цикл, пока поток не вернет `None`, сигнализируя о своем завершении. Поскольку наш метод `poll_next` никогда не возвращает `None`, это фактически бесконечный цикл, поэтому задача `print_keypresses` никогда не завершается. +Мы используем `while let` для цикла, пока стрим не вернет `None`, сигнализируя о своем завершении. Поскольку наш метод `poll_next` никогда не возвращает `None`, это фактически бесконечный цикл, поэтому задача `print_keypresses` никогда не завершается. Давайте добавим задачу `print_keypresses` в наш исполнитель в `main.rs`, чтобы снова получить работающий ввод с клавиатуры: @@ -1420,11 +1420,11 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { Если вы будете следить за загрузкой процессора вашего компьютера, вы увидите, что процесс `QEMU` теперь постоянно загружает CPU. Это происходит потому, что наш `SimpleExecutor` многократно опрашивает задачи в цикле. Поэтому даже если мы не нажимаем никаких клавиш на клавиатуре, исполнитель снова и снова вызывает `poll` для нашей задачи `print_keypresses`, хотя задача не может добиться прогресса и будет каждый раз возвращать `Poll::Pending`. -### Executor with Waker Support +### Исполнитель с Поддержкой Waker -Чтобы решить проблему производительности, нам нужно создать исполнитель, который правильно использует уведомления `Waker`. Таким образом, исполнитель будет уведомлен, когда произойдет следующее прерывание клавиатуры, и ему не нужно будет постоянно опрашивать задачу `print_keypresses`. +Чтобы решить проблему производительности, нам нужно создать исполнитель, который правильно использует уведомления `Waker`. Так исполнитель будет уведомлен при следующем прерывании клавиатуры и ему не нужно будет постоянно опрашивать задачу `print_keypresses`. -#### Task Id +#### Id Задачи Первый шаг в создании исполнителя с правильной поддержкой уведомлений waker — это дать каждой задаче уникальный идентификатор. Это необходимо, потому что нам нужно иметь способ указать, какую задачу следует разбудить. Мы начинаем с создания нового типа-обёртки `TaskId`: @@ -1452,7 +1452,7 @@ impl TaskId { } ``` -Функция использует статическую переменную `NEXT_ID` типа [`AtomicU64`], чтобы гарантировать, что каждый идентификатор присваивается только один раз. Метод [`fetch_add`] атомарно увеличивает значение и возвращает предыдущее за одну атомарную операцию. Это значит, что даже когда метод `TaskId::new` вызывается параллельно, каждый идентификатор возвращается ровно один раз. Параметр [`Ordering`] определяет, может ли компилятор переупорядочить операцию `fetch_add` в потоке инструкций. Поскольку мы только требуем, чтобы идентификатор был уникальным, в этом случае достаточно использования упорядочивание `Relaxed` с самыми слабыми требованиями. +Функция использует статическую переменную `NEXT_ID` типа [`AtomicU64`], чтобы гарантировать, что каждый идентификатор присваивается только один раз. Метод [`fetch_add`] атомарно увеличивает значение и возвращает предыдущее за одну атомарную операцию. Это значит, что даже когда метод `TaskId::new` вызывается параллельно, каждый идентификатор возвращается ровно один раз. Параметр [`Ordering`] определяет, может ли компилятор переупорядочить операцию `fetch_add` в стриме инструкций. Поскольку мы только требуем, чтобы идентификатор был уникальным, в этом случае достаточно использования упорядочивание `Relaxed` с самыми слабыми требованиями. [`AtomicU64`]: https://doc.rust-lang.org/core/sync/atomic/struct.AtomicU64.html [`fetch_add`]: https://doc.rust-lang.org/core/sync/atomic/struct.AtomicU64.html#method.fetch_add @@ -1480,7 +1480,7 @@ impl Task { Новое поле `id` позволяет уникально называть задачу, что необходимо для пробуждения конкретной задачи. -#### The `Executor` Type +#### Тип `Executor` Мы создаем наш новый тип `Executor` в модуле `task::executor`: @@ -1517,7 +1517,7 @@ impl Executor { Вместо хранения задач в [`VecDeque`], как мы делали для нашего `SimpleExecutor`, мы используем `task_queue` с идентификаторами задач и [`BTreeMap`] с именем `tasks`, который содержит фактические экземпляры `Task`. Карта индексируется по `TaskId`, что позволяет эффективно продолжать выполнение конкретной задачи. -Поле `task_queue` представляет собой [`ArrayQueue`] идентификаторов задач, обёрнутую в тип [`Arc`], который реализует _счётчик ссылок_ (_reference counting_). Счётчик ссылок позволяет разделять владение значением между несколькими владельцами. Он создает место в куче и записывает туда кол-во активных ссылок. Когда количество активных ссылок достигает нуля, значение больше не нужно и может быть освобождено. +Поле `task_queue` представляет собой [`ArrayQueue`] идентификаторов задач, обёрнутую в тип [`Arc`], который реализует _счётчик ссылок_ (_reference counting_). Счётчик ссылок позволяет разделять владение значением между несколькими владельцами. Он аллоцирует место куче и записывает туда кол-во активных ссылок. Когда количество активных ссылок достигает нуля, значение больше не нужно и может быть освобождено. Мы используем тип `Arc` для `task_queue`, потому что он будет разделяться между исполнителем и wakers. Идея в том, что wakers добавляют идентификатор разбуженной задачи в очередь. Исполнитель находится на приемной стороне очереди, извлекает разбуженные задачи по их идентификатору из `tasks` дерева и затем выполняет их. Причина использования фиксированной очереди вместо неограниченной, такой как [`SegQueue`] в том, что обработчики прерываний не должны выделять память при добавлении в эту очередь. @@ -1528,7 +1528,7 @@ impl Executor { Чтобы создать `Executor`, мы предоставляем простую функцию `new`. Мы выбираем ёмкость 100 для `task_queue`, что должно быть более чем достаточно на обозримое будущее. В случае, если в нашей системе в какой-то момент будет больше 100 параллельных задач, мы можем легко увеличить этот размер. -#### Spawning Tasks +#### Cпавн Задач Как и в `SimpleExecutor`, мы предоставляем метод `spawn` для нашего типа `Executor`, который добавляет данную задачу в дерево `tasks` и немедленно пробуждает её, добавляя её идентификатор в `task_queue`: @@ -1548,7 +1548,7 @@ impl Executor { Если в карте уже существует задача с тем же идентификатором, метод [`BTreeMap::insert`] возвращает её. Это никогда не должно происходить, поскольку каждая задача имеет уникальный идентификатор, поэтому в этом случае мы вызываем панику, так как это указывает на ошибку в нашем коде. Аналогично, мы вызываем панику, когда `task_queue` полна, так как этого никогда не должно происходить, если мы выбираем достаточно большой размер очереди. -#### Running Tasks +#### Запуск Задач Чтобы выполнить все задачи в `task_queue`, мы создаём приватный метод `run_ready_tasks`: @@ -1609,7 +1609,7 @@ impl Executor { [`BTreeMap::remove`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html#method.remove -#### Waker Design +#### Архитектура Waker Задача waker — добавить идентификатор разбуженной задачи в `task_queue` исполнителя. Мы реализуем это, создавая новую структуру `TaskWaker`, которая хранит идентификатор задачи и ссылку на `task_queue`: @@ -1668,7 +1668,7 @@ impl Wake for TaskWaker { Разница между методами `wake` и `wake_by_ref` заключается в том, что последний требует только ссылки на `Arc`, в то время как первый забирает владение `Arc` и, следовательно, часто требует увеличения счётчика ссылок. Не все типы поддерживают пробуждение по ссылке, поэтому реализация метода `wake_by_ref` является необязательной. Однако это может привести к лучшей производительности, так как избегает ненужных модификаций счётчика ссылок. В нашем случае мы можем просто перенаправить оба метода трейта к нашей функции `wake_task`, которая требует только совместимой ссылки `&self`. -##### Creating Wakers +##### Создание Wakers Поскольку тип `Waker` поддерживает преобразования [`From`] для всех значений, обёрнутых в `Arc`, которые реализуют трейт `Wake`, мы теперь можем реализовать функцию `TaskWaker::new`, которая требуется для метода `Executor::run_ready_tasks`: @@ -1691,7 +1691,7 @@ impl TaskWaker { [waker-from-impl]: https://github.com/rust-lang/rust/blob/cdb50c6f2507319f29104a25765bfb79ad53395c/src/liballoc/task.rs#L58-L87 -#### A `run` Method +#### Метод `run` С нашей реализацией waker мы наконец можем создать метод `run` для нашего исполнителя: @@ -1736,7 +1736,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { Тем не менее загрузка процессора QEMU не уменьшилась. Причина в том, что мы по-прежнему загружаем процессор всё время. Мы больше не опрашиваем задачи, пока они не будут пробуждены снова, но мы всё же проверяем `task_queue` в цикле с занятым ожиданием. Чтобы это исправить, нам нужно перевести процессор в спящий режим, если больше нет работы. -#### Sleep If Idle +#### Спать если Idle Основная идея в том, чтобы выполнять [инструкцию `hlt`][[`hlt` instruction]] при пустой `task_queue`. Эта инструкция ставит процессор в спящий режим до следующего прерывания. Факт, что процессор немедленно активируется снова при возникновении прерывания, обеспечивает возможность прямой реакции, когда обработчик прерываний добавляет задачу в `task_queue`. @@ -1806,7 +1806,7 @@ impl Executor { Теперь наш исполнитель правильно ставит процессор в спящий режим, когда задач нету. Мы можем видеть, что загрузка процессора QEMU значительно снизилась, когда мы снова запускаем наше ядро с помощью `cargo run`. -#### Possible Extensions +#### Возможные Расширения Наш исполнитель теперь способен эффективно выполнять задачи. Он использует уведомления waker, чтобы избежать опроса ожидающих задач, и переводит процессор в спящий режим, когда задач нету. Однако наш исполнитель всё ещё довольно примитивный, и существует множество способов расширить его функциональность: @@ -1820,7 +1820,7 @@ impl Executor { [scheduling-wiki]: https://en.wikipedia.org/wiki/Scheduling_(computing) [_work stealing_]: https://en.wikipedia.org/wiki/Work_stealing -## Summary +## Итоги Мы начали этот пост с введения в **мультизадачность** и различия между _вытесняемой_ мультизадачностью, которая регулярно принудительно прерывает выполняющиеся задачи, и _кооперативной_ мультизадачностью, которая позволяет задачам выполняться до тех пор, пока они добровольно не отдадут управление процессором. @@ -1832,7 +1832,7 @@ impl Executor { Чтобы использовать уведомления waker для задачи клавиатуры, мы создали новый тип `Executor`, который использует совместно используемую `task_queue` на основе `Arc` для готовых задач. Мы реализовали тип `TaskWaker`, который добавляет идентификаторы разбуженных задач непосредственно в эту `task_queue`, которые затем снова опрашиваются исполнителем. Чтобы сэкономить энергию, когда нет запущенных задач, мы добавили поддержку перевода процессора в спящий режим с использованием инструкции `hlt`. Наконец, мы обсудили некоторые потенциальные расширения для нашего исполнителя, например, предоставление поддержки многопроцессорности. -## What's Next? +## Что Далее? Используя async/await, мы теперь имеем базовую поддержку кооперативной мультизадачности в нашем ядре. Хотя кооперативная мультизадачность очень эффективна, она может привести к проблемам с задержкой, когда отдельные задачи выполняются слишком долго, тем самым препятствуя выполнению других задач. По этой причине имеет смысл также добавить поддержку вытесняющей мультизадачности в наше ядро. From 2a1d054fee095af5ed8eb953a7ef3d30fcdc1834 Mon Sep 17 00:00:00 2001 From: TakiMoysha <36836047+TakiMoysha@users.noreply.github.com> Date: Mon, 15 Sep 2025 23:59:24 +0200 Subject: [PATCH 7/9] made corrections --- .../posts/12-async-await/index.ru.md | 127 +++++++++--------- 1 file changed, 65 insertions(+), 62 deletions(-) diff --git a/blog/content/edition-2/posts/12-async-await/index.ru.md b/blog/content/edition-2/posts/12-async-await/index.ru.md index 45885dbf..c14f4c87 100644 --- a/blog/content/edition-2/posts/12-async-await/index.ru.md +++ b/blog/content/edition-2/posts/12-async-await/index.ru.md @@ -13,6 +13,7 @@ translators = ["TakiMoysha"] +++ + В этом посте мы рассмотрим _кооперативную многозадачность_ и возможности _async/await_ в Rust. Мы подробно рассмотрим, как async/await работает в Rust, включая трейт `Future`, переходы в конечных автоматах и _закрепления_ (pinning). Мы добавим базовую поддержку async/await в наше ядро путем создания асинхронных задач обработки ввода с клавиатуры и базовый исполнитель (executor). @@ -67,7 +68,7 @@ translators = ["TakiMoysha"] Недостатком вытесняющей многозадачности в том, что каждой задаче требуется собственный стек. По сравнению с общим стеком это приводит к более высокому использованию памяти на задачу и часто ограничивает количество задач в системе. Другим недостатком является то, что ОС всегда должна сохранять полное состояние регистров ЦП при каждом переключении задач, даже если задача использовала только небольшую часть регистров. -Вытесняющая многозадачность и потоки - фундаментальные компонтенты ОС, т.к. они позволяют запускать неизвестные программы в userspace . Мы подробнее обсудим эти концепции в будущийх постах. Однако сейчас, мы сосредоточимся на кооперативной многозадачности, которая также предоставляет полезные возможности для нашего ядра. +Вытесняющая многозадачность и потоки - фундаментальные компоненты ОС, т.к. они позволяют запускать неизвестные программы в userspace. Мы подробнее обсудим эти концепции в будущих постах. Однако сейчас, мы сосредоточимся на кооперативной многозадачности, которая также предоставляет полезные возможности для нашего ядра. ### Кооперативная Многозадачность @@ -87,7 +88,7 @@ translators = ["TakiMoysha"] Поскольку задачи сами определяют точки паузы, им не нужно, чтобы ОС сохраняла их состояние. Вместо этого они могут сохранять то состояние, которое необходимо для продолжения работы, что часто приводит к улучшению производительности. Например, задаче, которая только что завершила сложные вычисления, может потребоваться только резервное копирование конечного результата вычислений, т.к. промежуточные результаты ей больше не нужны. -Поддерживаемые языком реализации кооперативной мультизадачности часто даже могут сохранять необходимые части стека вызовов перед паузой. Например, реализация async/await в Rust сохраняет все локальные переменные, которые еще нужны, в автоматически сгенерированной структуре (см. ниже). Благодаря резервному копированию соответствующих частей стека вызовов перед приостановкой все задачи могут использовать один стек вызовов, что приводит к значительному снижению потребления памяти на задачу. Это позволяет создавать практически любое количество кооперативных задач без исчерпания памяти. +Поддерживаемые языком реализации кооперативной многозадачность часто даже могут сохранять необходимые части стека вызовов перед паузой. Например, реализация async/await в Rust сохраняет все локальные переменные, которые еще нужны, в автоматически сгенерированной структуре (см. ниже). Благодаря резервному копированию соответствующих частей стека вызовов перед приостановкой все задачи могут использовать один стек вызовов, что приводит к значительному снижению потребления памяти на задачу. Это позволяет создавать практически любое количество кооперативных задач без исчерпания памяти. #### Обсуждение @@ -230,7 +231,7 @@ fn file_len() -> impl Future { ##### Преимущества -Большое преимущество комбинаторов футур (future combinators) в том, что они сохраняют асинхронность. В сочетании с асинхронными интерфейсами ввода-вывода такой подход может обеспечить очень высокую производительность. То, что future кобинаторы реализованы как обычные структуры с имплементацией трейтов, позволяет компилятору чрезвычайно оптимизировать их. Подробнее см. в посте [_Futures с нулевой стоимостью в Rust_], где было объявлено о добавлении futures в экосистему Rust. +Большое преимущество комбинаторов футур (future combinators) в том, что они сохраняют асинхронность. В сочетании с асинхронными интерфейсами ввода-вывода такой подход может обеспечить очень высокую производительность. То, что future комбинаторы реализованы как обычные структуры с имплементацией трейтов, позволяет компилятору чрезвычайно оптимизировать их. Подробнее см. в посте [_Futures с нулевой стоимостью в Rust_], где было объявлено о добавлении futures в экосистему Rust. [_Futures с нулевой стоимостью в Rust_]: https://aturon.github.io/blog/2016/08/11/futures/ @@ -304,29 +305,29 @@ async fn example(min_len: usize) -> String { ([Попробовать](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=d93c28509a1c67661f31ff820281d434)) -Эта ф-ция - прямой перевод `example` написанной [выше](#Недостатки), которая использовала комбинаторы. Используя оператор `.await`, мы можем получить значение футуры без необходимости использования каких-либо замыканий или типов `Either`. В результате, мы можем писать наш код так же, как если бы это был обычный синхронный код, с той лишь разницей, что _это все еще асинхронный код_. +Эта функция - прямой перевод `example` написанной [выше](#Недостатки), которая использовала комбинаторы. Используя оператор `.await`, мы можем получить значение футуры без необходимости использования каких-либо замыканий или типов `Either`. В результате мы можем писать наш код так же, как если бы это был обычный синхронный код, с той лишь разницей, что _это все еще асинхронный код_. -#### Преобразованиe Конечных Автоматов +#### Преобразования Конечных Автоматов -За кулисами компилятор преобразует тело функции `async` в [_конечный автомат_][_state machine_] (state machine) с каждым вызовом `.await`, представляющим собой разное состояние. Для вышеуказанной ф-ции `example`, компилятор создает конечный автомат с четырьмя состояниями. +За кулисами компилятор преобразует тело функции `async` в [_конечный автомат_][_state machine_] (state machine) с каждым вызовом `.await`, представляющим собой разное состояние. Для вышеуказанной функции `example`, компилятор создает конечный автомат с четырьмя состояниями. [_state machine_]: https://en.wikipedia.org/wiki/Finite-state_machine ![Четыре состояния: start, waiting on foo.txt, waiting on bar.txt, end](async-state-machine-states.svg) -Каждое состояние представляет собой точку остановки в функции. Состояния _"Start"_ и _"End"_, указывают на начало и конец выполнения ф-ции. Состояние _"waiting on foo.txt"_ - функция в данный момент ждёт первого результата `async_read_file`. Аналогично, состояние _"waiting on bar.txt"_ представляет остановку, когда ф-ция ожидает второй результат `async_read_file`. +Каждое состояние представляет собой точку остановки в функции. Состояния _"Start"_ и _"End"_, указывают на начало и конец выполнения ф-ции. Состояние _"waiting on foo.txt"_ - функция в данный момент ждёт первого результата `async_read_file`. Аналогично, состояние _"waiting on bar.txt"_ представляет остановку, когда функция ожидает второй результат `async_read_file`. Конечный автомат реализует trait `Future` делая каждый вызов `poll` возможным переход между состояниями: ![Четыре состояния и переходы: start, waiting on foo.txt, waiting on bar.txt, end](async-state-machine-basic.svg) -Диаграмма использует стрелки для представления переключений состояний и ромбы для представления альтернативных путей. Например, если файл `foo.txt` не готов, то мы используется путь _"no"_ переходя в состояние _"waiting on foo.txt"_. Иначе, используется путь _"yes"_. Где маленький красный ромб без подписи - ветвь ф-ции exmple, где `if content.len() < 100`. +Диаграмма использует стрелки для представления переключений состояний и ромбы для представления альтернативных путей. Например, если файл `foo.txt` не готов, то мы идем по пути _"no"_ переходя в состояние _"waiting on foo.txt"_. Иначе, используется путь _"yes"_. Где маленький красный ромб без подписи - ветвь функции example, где `if content.len() < 100`. -Мы видим, что первый вызов `poll` запускает функцию и она выполняться до тех пор, пока у футуры не будет результата. Если все футуры на пути готовы, ф-ция может выполниться до состояния _"end"_ , то есть вернуть свой результат, завернутый в `Poll::Ready`. В противном случае конечный автомат переходит в состояние ожидания и возвращает `Poll::Pending`. При следующем вызове `poll` машина состояний начинает с последнего состояния ожидания и повторяет последнюю операцию. +Мы видим, что первый вызов `poll` запускает функцию и она выполняться до тех пор, пока у футуры не будет результата. Если все футуры на пути готовы, функция может выполниться до состояния _"end"_ , то есть вернуть свой результат, завернутый в `Poll::Ready`. В противном случае конечный автомат переходит в состояние ожидания и возвращает `Poll::Pending`. При следующем вызове `poll` машина состояний начинает с последнего состояния ожидания и повторяет последнюю операцию. #### Сохранение состояния -Для продолжнеия работы с последнего состояния ожидания, автомат должен отслеживать текущее состояние внутри себя. Еще, он должен сохранять все переменные, которые необходимы для продолжнеия выполнения при следующем вызове `poll`. Здесь компилятор действительно может проявить себя: зная, когда используются те или иные переменные, он может автоматически создавать структуры с точным набором требуемых переменных. +Для продолжения работы с последнего состояния ожидания, автомат должен отслеживать текущее состояние внутри себя. Еще, он должен сохранять все переменные, которые необходимы для продолжения выполнения при следующем вызове `poll`. Здесь компилятор действительно может проявить себя: зная, когда используются те или иные переменные, он может автоматически создавать структуры с точным набором требуемых переменных. Например, компилятор генерирует структуры для вышеприведенной ф-ции `example`: @@ -446,7 +447,7 @@ ExampleStateMachine::WaitingOnFooTxt(state) => { } ``` -Эта ветка `match` начинавется с вызова `poll` для `foo_txt_future`. Если она не готова, мы выходим из цикла и возвращаем `Poll::Pending`. В этом случае `self` остается в состоянии `WaitingOnFooTxt`, следующий опрос `poll` автомата попадёт в ту же ветку `match` и повторит проверку готовности `foo_txt_future`. +Эта ветка `match` начинается с вызова `poll` для `foo_txt_future`. Если она не готова, мы выходим из цикла и возвращаем `Poll::Pending`. В этом случае `self` остается в состоянии `WaitingOnFooTxt`, следующий опрос `poll` автомата попадёт в ту же ветку `match` и повторит проверку готовности `foo_txt_future`. Когда `foo_txt_future` готов, мы присваиваем результат переменной `content` и продолжаем выполнять код функции `example`: Если `content.len()` меньше чем `min_len`, из структуры состояния, файл `bar.txt` читается асинхронно. Мы ещё раз переводим операцию `.await` в изменение состояния, теперь в состояние `WaitingOnBarTxt`. Пока мы выполняем `match` внутри цикла, исполнение позже переходит к ветке `match` нового состояния, где проверяется готовность `bar_txt_future`. @@ -467,9 +468,9 @@ ExampleStateMachine::WaitingOnBarTxt(state) => { } ``` -Аналогично состоянию `WaitingOnFooTxt`, мы начинаем с проверки готовности `bar_txt_future`. Если она ещё не готова, мы выходим из цикла и возвращаем `Poll::Pending`. В противном случае, мы можем выполнить последнюю операцию функции `example`: конкатенацию переменной `content` с результатом футуры. Переводим автомат в состояние `End` и затем возвращаем результат обёрнутый в `Poll::Ready`. +Аналогично состоянию `WaitingOnFooTxt`, мы начинаем с проверки готовности `bar_txt_future`. Если она ещё не готова, мы выходим из цикла и возвращаем `Poll::Pending`. В противном случае мы можем выполнить последнюю операцию функции `example`: конкатенацию переменной `content` с результатом футуры. Переводим автомат в состояние `End` и затем возвращаем результат обёрнутый в `Poll::Ready`. -В итоге, код для `End` состояния выглядит так: +В итоге код для `End` состояния выглядит так: ```rust ExampleStateMachine::End(_) => { @@ -505,7 +506,7 @@ fn example(min_len: usize) -> ExampleStateMachine { #### Закрепление -Мы уже несколько раз столкнулись с понятием _закрепления_ (pinnig, пиннинг) в этом посте. Наконец, время чтобы изучить, что такое закрепление и почему оно необходимо. +Мы уже несколько раз столкнулись с понятием _закрепления_ (pinnig, пиннинг) в этом посте. Наконец, время, чтобы изучить, что такое закрепление и почему оно необходимо. #### Самоссылающиеся структуры @@ -537,11 +538,11 @@ struct WaitingOnWriteState { Внутренний указатель нашей самоссылочной структуры приводит к базовой проблеме, которая становится очевидной, когда мы посмотрим на её раскладку памяти: -![массив от 0x10014 с полями 1, 2, и 3; элемент в адресе 0x10020, указывающий на последний массив-элемент в 0x1001c](self-referential-struct.svg) +![массив от 0x10014 с полями 1, 2, и 3; элемент в адресе 0x10020, указывающий на последний элемент массива в 0x1001c](self-referential-struct.svg) Поле `array` начинается в адресе `0x10014`, а поле `element` - в адресе `0x10020`. Оно указывает на адрес `0x1001c`, потому что последний элемент массива находится там. В этот момент все ещё в порядке. Однако проблема возникает, когда мы перемещаем эту структуру на другой адрес памяти: -![массив от 0x10024 с полями 1, 2, и 3; элемент в адресе 0x10030, продолжающий указывать на 0x1001c, хотя последний массив-элемент сейчас находится в 0x1002c](self-referential-struct-moved.svg) +![массив от 0x10024 с полями 1, 2, и 3; элемент в адресе 0x10030, продолжающий указывать на 0x1001c, хотя последний элемент массива сейчас находится в 0x1002c](self-referential-struct-moved.svg) Мы переместили структуру немного так, чтобы она теперь начиналась в адресе `0x10024`. Это могло произойти, например, когда мы передаем структуру как аргумент функции или присваиваем ей другое переменной стека. Проблема заключается в том, что поле `element` все ещё указывает на адрес `0x1001c`, хотя последний элемент массива теперь находится в адресе `0x1002c`. Поэтому указатель висит, с результатом неопределённого поведения на следующем вызове `poll`. @@ -558,12 +559,12 @@ struct WaitingOnWriteState { Rust выбрал третий подход из-за принципа предоставления _бесплатных абстракций_ (zero-cost abstractions), что означает, что абстракции не должны накладывать дополнительные расходы времени выполнения. API [_pinning_] предлагалось для решения этой проблемы в RFC 2349 (). В следующем разделе мы дадим краткий обзор этого API и объясним, как оно работает с async/await и futures. #### Значения в Куче (Heap) - -Первый наблюдение состоит в том, что значения [аллоцированные в куче], обычно имеют фиксированный адрес памяти. Они создаются с помощью вызова `allocate` и затем ссылаются на тип указателя, такой как `Box`. Хотя перемещение указательного типа возможно, значение кучи, которое указывает на него, остается в том же адресе памяти до тех пор, пока оно не будет освобождено с помощью вызова `deallocate` еще раз. +аллоцированные +Первое наблюдение состоит в том, что значения [аллоцированные в куче], обычно имеют фиксированный адрес памяти. Они создаются с помощью вызова `allocate` и затем ссылаются на тип указателя, такой как `Box`. Хотя перемещение указательного типа возможно, значение кучи, которое указывает на него, остается в том же адресе памяти до тех пор, пока оно не будет освобождено с помощью вызова `deallocate` еще раз. [аллоцированные в куче]: @/edition-2/posts/10-heap-allocation/index.md -Используя аллокацию на куче, можно попытаться создать самоссылающуюся структуру: +Аллоцируя в куче, можно попытаться создать самоссылающуюся структуру: ```rust fn main() { @@ -585,11 +586,11 @@ struct SelfReferential { [playground-self-ref]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=ce1aff3a37fcc1c8188eeaf0f39c97e8 -Мы создаем простую структуру с названием `SelfReferential`, которая содержит только одно поле c указателем. Во-первых, мы инициализируем эту структуру с пустым указателем и затем выделяем место в куче с помощью `Box::new`. Затем мы определяем адрес кучи для выделенной структуры и храним его в переменной `ptr`. В конце концов, мы делаем структуру самоссылающейся, назначив переменную `ptr` полю `self_ptr`. +Мы создаем простую структуру с названием `SelfReferential`, которая содержит только одно поле с указателем. Во-первых, мы инициализируем эту структуру с пустым указателем и затем выделяем место в куче с помощью `Box::new`. Затем мы определяем адрес кучи для выделенной структуры и храним его в переменной `ptr`. В конце концов, мы делаем структуру самоссылающейся, назначив переменную `ptr` полю `self_ptr`. Когда мы запускаем этот код в [песочнице][playground-self-ref], мы видим, что адрес на куче и внутренний указатель равны, что означает, что поле `self_ptr` валидное. Поскольку переменная `heap_value` является только указателем, перемещение его (например, передачей в функцию) не изменяет адрес самой структуры, поэтому `self_ptr` остается действительным даже при перемещении указателя. -Тем не менее, все еще есть путь сломать этот пример: мы можем выйти из `Box` или изменить содержимое: +Тем не менее все еще есть путь сломать этот пример: мы можем выйти из `Box` или изменить содержимое: ```rust let stack_value = mem::replace(&mut *heap_value, SelfReferential { @@ -611,7 +612,7 @@ println!("internal reference: {:p}", stack_value.self_ptr); #### `Pin>` и `Unpin` -API _закрепления_ предоставляет решение проблемы `&mut T` в виде типа-обертки [`Pin`] и трейта-маркера [`Unpin`]. Идея использования - ограничить все методы `Pin`, которые могут быть использованы для получения ссылок `&mut` на обернутое значение (например, [`get_mut`][pin-get-mut] или [`deref_mut`][pin-deref-mut]), на трейт `Unpin`. Трейт `Unpin` является _авто трейтом_ ([_auto trait_]), который автоматически реализуется для всех типов, за исключением тех, которые явно отказываются от него. Заставляя самоссылающиеся структуры отказаться от `Unpin`, не остается (безопасного) способа получить `&mut T` из типа `Pin>` для них. В результате их внутренние самоссылки гарантированно остаются действительными. +API _закрепления_ предоставляет решение проблемы `&mut T` в виде типа-обертки [`Pin`] и трейта-маркера [`Unpin`]. Идея использования - ограничить все методы `Pin`, которые могут быть использованы для получения ссылок `&mut` на обернутое значение (например, [`get_mut`][pin-get-mut] или [`deref_mut`][pin-deref-mut]), на трейт `Unpin`. Трейт `Unpin` является _авто трейтом_ ([_auto trait_]), то есть автоматически реализуется для всех типов, кроме тех, которые явно отказываются от него. Заставляя самоссылающиеся структуры отказаться от `Unpin`, не остается (безопасного) способа получить `&mut T` из типа `Pin>` для них. В результате их внутренние самоссылки гарантированно остаются действительными. [`Pin`]: https://doc.rust-lang.org/stable/core/pin/struct.Pin.html [`Unpin`]: https://doc.rust-lang.org/nightly/std/marker/trait.Unpin.html @@ -688,7 +689,7 @@ unsafe { [`Pin::as_mut`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.as_mut -Теперь единственной оставшейся ошибкой является желаемая ошибка на `mem::replace`. Помните, что эта операция пытается переместить значение, размещённое в куче, на стек, что нарушило бы самоссылку, хранящуюся в поле `self_ptr`. Отказываясь от `Unpin` и используя `Pin>`, мы можем предотвратить эту операцию на этапе компиляции и таким образом безопасно работать с самоссыльными структурами. Как мы видели, компилятор не может доказать, что создание самоссылки безопасно (пока), поэтому нам нужно использовать небезопасный блок и самостоятельно проверить корректность. +Теперь единственной оставшейся ошибкой является желаемая ошибка на `mem::replace`. Помните, что эта операция пытается переместить значение, размещённое в куче, на стек, что нарушило бы самоссылку, хранящуюся в поле `self_ptr`. Отказываясь от `Unpin` и используя `Pin>`, мы можем предотвратить эту операцию на этапе компиляции и безопасно работать с самоссыльными структурами. Как мы видели, компилятор не может доказать, что создание самоссылки безопасно (пока), поэтому нам нужно использовать небезопасный блок и самостоятельно проверить корректность. #### Закрепление в стеке и `Pin<&mut T>` @@ -736,7 +737,7 @@ fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll Цель исполнителя в том, чтобы позволить создавать футуры в качестве независимых задач, обычно через какой-либо метод `spawn`. Исполнитель затем отвечает за опрос всех футур, пока они не завершатся. Большое преимущество управления всеми футурами в одном месте состоит в том, что исполнитель может переключаться на другую футуру, когда текущая футура возвращает `Poll::Pending`. Таким образом, асинхронные операции выполняются параллельно, и процессор остаётся загруженным. -Многие реализации исполнителей также могут использовать преимущества систем с мультиядерными процессорами. Они создают [пул потоков][thread pool], способный использовать все ядра, если достаточно работы, и применяют такие техники, как [work stealing], для балансировки нагрузки между ядрами. Существуют также специальные реализации исполнителей для встроенных систем, которые оптимизируют низкую задержку и затраты памяти. +Многие реализации исполнителей также могут использовать преимущества систем с многоядерными процессорами. Они создают [пул потоков][thread pool], способный использовать все ядра, если достаточно работы, и применяют такие техники, как [work stealing], для балансировки нагрузки между ядрами. Существуют также специальные реализации исполнителей для встроенных систем, которые оптимизируют низкую задержку и затраты памяти. [thread pool]: https://en.wikipedia.org/wiki/Thread_pool [work stealing]: https://en.wikipedia.org/wiki/Work_stealing @@ -761,13 +762,13 @@ async fn write_file() { Мы увидим, как работает тип `Waker` в деталях, когда создадим свой собственный исполнитель с поддержкой waker в разделе реализации этого поста. -### Кооперативная Мультизадачность? +### Кооперативная Многозадачности? В начале этого поста мы говорили о вытесняющей (preemptive) и кооперативной (cooperative) многозадачности. В то время как вытесняющая многозадачность полагается на операционную систему для принудительного переключения между выполняемыми задачами, кооперативная многозадачность требует, чтобы задачи добровольно уступали контроль над CPU через операцию _yield_ на регулярной основе. Большое преимущество кооперативного подхода в том, что задачи могут сохранять своё состояние самостоятельно, что приводит к более эффективным переключениям контекста и делает возможным совместное использование одного и того же стека вызовов между задачами. Это может не быть сразу очевидным, но футуры и async/await представляют собой реализацию кооперативного паттерна многозадачности: -- Каждая футура, добавленная в исполнитель, по сути является кооперативной задачей. +- Каждая футура, добавленная в исполнителя, по сути является кооперативной задачей. - Вместо использования явной операции yield, футуры уступают контроль над ядром CPU, возвращая `Poll::Pending` (или `Poll::Ready` в конце). - Нет ничего, что заставляло бы футуру уступать ЦПУ. Если они захотят, они могут никогда не возвращать ответ на `poll`, например, бесконечно выполняя цикл. - Поскольку каждая футура может блокировать выполнение других футур в исполнителе, нам нужно доверять им, чтобы они не были вредоносными (malicious). @@ -779,7 +780,7 @@ async fn write_file() { ## Реализация -Теперь, когда мы понимаем, как работает кооперативная многозадачность на основе футур и async/await в Rust, пора добавить поддержку этого в наш ядро. Поскольку трейт [`Future`] является частью библиотеки `core`, а async/await — это особенность самого языка, нам не нужно делать ничего особенного, чтобы использовать его в нашем `#![no_std]` ядре. Единственное требование — использовать, как минимум, nightly версию Rust от `2020-03-25`, поскольку до этого времени async/await не поддерживала `no_std`. +Теперь, когда мы понимаем, как работает кооперативная многозадачность на основе футур и async/await в Rust, пора добавить поддержку этого в наше ядро. Поскольку трейт [`Future`] является частью библиотеки `core`, а async/await — это особенность самого языка, нам не нужно делать ничего особенного, чтобы использовать его в нашем `#![no_std]` ядре. Единственное требование — использовать, как минимум, nightly версию Rust от `2020-03-25`, поскольку до этого времени async/await не поддерживала `no_std`. С достаточно свежей nightly версией мы можем начать использовать async/await в нашем `main.rs`: @@ -800,7 +801,7 @@ async fn example_task() { Чтобы запустить футуру, которую вернул `example_task`, нам нужно вызывать `poll` на ней, пока он не сигнализирует о своём завершении, возвращая `Poll::Ready`. Для этого нам нужно создать простой тип исполнителя. -### Задачи +### Задачи (Таски) Перед тем как начать реализацию исполнителя, мы создаем новый модуль `task` с типом `Task`: @@ -821,7 +822,7 @@ pub struct Task { } ``` -Структура `Task` является обёрткой вокруг _закрепленной_, _размещённой в куче_ и _динамически диспетчеризуемой футуры_ с пустым типом `()` в качестве выходного значения. Разберём её подробнее: +Структура `Task` является обёрткой вокруг _закрепленной_, _размещённой в куче_ и _динамически диспетчерезуемой_ футуры с пустым типом `()` в качестве выходного значения. Разберём её подробнее: - Мы требуем, чтобы футура, связанная с задачей, возвращала `()`. Это означает, что задачи не возвращают никаких результатов, они просто выполняются для побочных эффектов. Например, функция `example_task`, которую мы определили выше, не имеет возвращаемого значения, но выводит что-то на экран как побочный эффект (side effect). - Ключевое слово `dyn` указывает на то, что мы храним [_trait object_] в `Box`. Это означает, что методы на футуре диспетчеризуются динамически, позволяя хранить в типе `Task` разные типы футур. Это важно, поскольку каждая `async fn` имеет свой собственный тип, и мы хотим иметь возможность создавать несколько разных задач. @@ -845,13 +846,13 @@ impl Task { } ``` -Функция принимает произвольную футуру с выходным типом `()` и закрепляет его в памяти через [`Box::pin`]. Затем она оборачивает упакованную футуру в структуру `Task` и возвращает ее. Здесь нужно время жизни`'static`, т.к. возвращаемый `Task` может жить произвольное время, следовательно, футура также должна быть действительнтой в течение этого времени. +Функция принимает произвольную футуру с выходным типом `()` и закрепляет его в памяти через [`Box::pin`]. Затем она оборачивает упакованную футуру в структуру `Task` и возвращает ее. Здесь нужно время жизни `'static`, т.к. возвращаемый `Task` может жить произвольное время, следовательно, футура также должна быть действительной в течение этого времени. Мы также добавляем метод `poll`, чтобы позволить исполнителю опрашивать хранимую футуру: ```rust -// in src/task/mod.rs +// src/task/mod.rs use core::task::{Context, Poll}; @@ -929,7 +930,7 @@ fn dummy_waker() -> Waker { ##### `RawWaker` -Тип [`RawWaker`] требует от программиста явного определения [_таблицы виртуальных методов_] (_vtable_), которая указывает функции, которые должны быть вызваны при клонировании (cloned), пробуждении (woken) или удалении (droppen) `RawWaker`. Расположение этой vtable определяется типом [`RawWakerVTable`]. Каждая функция получает аргумент `*const ()`, который является _type-erased_ указателем на некоторое значение. Причина использования указателя `*const ()` вместо правильной ссылки в том, что тип `RawWaker` должен быть non-generic, но при этом поддерживать произвольные типы. Указатель передается в аргументе `data` ф-ции [`RawWaker::new`], которая просто инициализирует `RawWaker`. Затем `Waker` использует этот `RawWaker`, чтобы вызывать функции vtable с `data`. +Тип [`RawWaker`] требует от программиста явного определения [_таблицы виртуальных методов_] (_vtable_), которая указывает функции, которые должны быть вызваны при клонировании (cloned), пробуждении (woken) или удалении (droppen) `RawWaker`. Расположение этой vtable определяется типом [`RawWakerVTable`]. Каждая функция получает аргумент `*const ()`, который является _type-erased_ указателем на некоторое значение. Причина использования указателя `*const ()` вместо правильной ссылки в том, что тип `RawWaker` должен быть non-generic, но при этом поддерживать произвольные типы. Указатель передается в аргументе `data` функции [`RawWaker::new`], которая просто инициализирует `RawWaker`. Затем `Waker` использует этот `RawWaker`, чтобы вызывать функции vtable с `data`. [_таблицы виртуальных методов_]: https://en.wikipedia.org/wiki/Virtual_method_table [`RawWakerVTable`]: https://doc.rust-lang.org/stable/core/task/struct.RawWakerVTable.html @@ -941,7 +942,7 @@ fn dummy_waker() -> Waker { [`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html [`Box::into_raw`]: https://doc.rust-lang.org/stable/alloc/boxed/struct.Box.html#method.into_raw -##### Заклушка `RawWaker` +##### Заглушка `RawWaker` Хотя вручную создавать `RawWaker` не рекомендуется, в настоящее время нет другого способа создать заглушку `Waker`, которая ничего не делает. К счастью, тот факт, что мы хотим ничего не делать, делает реализацию функции `dummy_raw_waker` относительно безопасной: @@ -1046,17 +1047,17 @@ async fn example_task() { [_Interrupts_]: @/edition-2/posts/07-hardware-interrupts/index.md -В дальнейшем мы создадим асинхронную задачу на основе прерывания клавиатуры. Это хороший кандидат, они недетерминированны и критичны по времени задержки. Недетерминированность означает, что невозможно предсказать, когда произойдёт нажатие клавиши, поскольку это полностью зависит от пользователя. Критичность ко времени задержки означает, что мы хотим обрабатывать ввод с клавиатуры своевременно, иначе пользователь почувствует задержку. Чтобы эффективно поддерживать такую задачу, исполнителю будет необходимо обеспечить надлежащую поддержку уведомлений `Waker`. +В дальнейшем мы создадим асинхронную задачу на основе прерываний с клавиатуры. Это хороший кандидат, такие прерывания недетерминированны и критичны по времени задержки. Недетерминированность означает, что невозможно предсказать, когда произойдёт нажатие клавиши, поскольку это полностью зависит от пользователя. Критичность ко времени задержки означает, что мы хотим обрабатывать ввод с клавиатуры своевременно, иначе пользователь почувствует задержку. Чтобы эффективно поддерживать такую задачу, исполнителю будет необходимо обеспечить надлежащую поддержку уведомлений `Waker`. #### Очередь Скан-кодов Сейчас мы обрабатываем ввод с клавиатуры непосредственно в обработчике прерываний. Это не лучший подход в долгосрочной перспективе, обработка прерываний должна выполняться как можно быстрее, так как они могут прерывать важную работу. Вместо этого обработчики прерываний должны выполнять только минимальный объем необходимой работы (например, считывание кода сканирования клавиатуры) и оставлять остальную работу (например, интерпретацию кода сканирования) фоновой задаче. -Распространённым шаблоном для делегирования работы фоновым задачам является очередь. Обработчик прерываний добавляет единицы работы в очередь, а фоновая задача обрабатывает работу в очереди. Применительно к нашем прерываниям это означает, что обработчик прерываний только считывает скан-код с клавиатуры, добавляет его в очередь, а затем возвращается. Задача клавиатуры находится на другом конце очереди и интерпретирует и обрабатывает каждый скан-код, который в неё добавляется: +Распространённым шаблоном для делегирования работы фоновым задачам является очередь. Обработчик прерываний добавляет единицы работы в очередь, а фоновая задача обрабатывает работу в очереди. Применительно к наим прерываниям это означает, что обработчик прерываний только считывает скан-код с клавиатуры, добавляет его в очередь, а затем возвращается. Задача клавиатуры находится на другом конце очереди и интерпретирует и обрабатывает каждый скан-код, который в неё добавляется: -![Очередь скан-кодов с 8 слотами вверху. Обработчик прерываний клавиатуры внизу слева с стрелкой "добавить скан-код" слева от очереди. Задача клавиатуры внизу справа со стрелкой "извлечь скан-код", идущей с правой стороны очереди.](scancode-queue.svg) +![Очередь скан-кодов с 8 слотами вверху. Обработчик прерываний с клавиатуры внизу слева со стрелкой "push скан-код" слева от очереди. Task клавиатуры внизу справа со стрелкой "pop скан-код", идущей с правой стороны очереди.](scancode-queue.svg) -Простая реализация такой очереди может быть основана на `VecDeque`, защищённом мьютексом. Однако использование мьютексов в обработчиках прерываний — не очень хорошая идея, так как это может легко привести к взаимным блокировкам (deadlock). Например, пользователь нажимает клавишу, но в тот же момент задача клавиатуру заблокировала очередь, обработчик прерываний пытается снова захватить блокировку и застревает навсегда. Ещё одна проблема с этим подходом в том, что `VecDeque` автоматически увеличивает свою ёмкость, выполняя новое выделение памяти в куче, когда она заполняется. Это также может привести к взаимным блокировкам, так как наш аллокатор также использует внутренний мьютекс. Более того, выделение памяти в куче может не получиться или занять значительное время, если куча фрагментирована. +Простая реализация такой очереди может быть основана на `VecDeque`, защищённом мьютексом. Однако использование мьютексов в обработчиках прерываний — не очень хорошая идея, так как это может легко привести к взаимным блокировкам (deadlock). Например, пользователь нажимает клавишу, но в тот же момент таска от клавиатуры заблокировала очередь, обработчик прерываний пытается снова захватить блокировку и застревает навсегда. Ещё одна проблема с этим подходом в том, что `VecDeque` автоматически увеличивает свою ёмкость, через аллокацию в куче, при заполнении. Это также может привести к взаимным блокировкам, так как наш аллокатор также использует внутренний мьютекс. Более того, выделение памяти в куче может не получиться или занять значительное время, если куча фрагментирована. Чтобы предотвратить эти проблемы, нам нужна реализация очереди, которая не требует мьютексов или выделений памяти для своей операции `push`. Такие очереди могут быть реализованы с использованием неблокирующих [атомарных операций][atiomic operations] для добавления и извлечения элементов. Таким образом, возможно создать операции `push` и `pop`, которые требуют только ссылки `&self` и могут использоваться без мьютекса. Чтобы избежать выделений памяти при `push`, очередь может быть основана на заранее выделенном буфере фиксированного размера. Хотя это делает очередь _ограниченной_ (_bounded_) (т.е. у неё есть максимальная длина), на практике часто возможно определить разумные верхние границы для длины очереди, так что это не представляет собой большой проблемы. @@ -1131,7 +1132,7 @@ use crate::println; /// вызывается обработчиком прерываний клавиатуры /// -/// не должен блокировать или аллоцировать. +/// не должен блокировать или аллоцировать память pub(crate) fn add_scancode(scancode: u8) { if let Ok(queue) = SCANCODE_QUEUE.try_get() { if let Err(_) = queue.push(scancode) { @@ -1143,7 +1144,7 @@ pub(crate) fn add_scancode(scancode: u8) { } ``` -Мы используем [`OnceCell::try_get`] для получения ссылки на инициализированную очередь. Если очередь ещё не инициализирована, мы игнорируем скан-код клавиатуры и выводим предупреждение. Важно, чтобы мы не пытались инициализировать очередь в этой функции, так как она будет вызываться обработчиком прерываний, который не должен выполнять выделения памяти в куче. Поскольку эта функция не должна быть доступна из нашего `main.rs`, мы используем видимость `pub(crate)`, чтобы сделать её доступной только для нашего `lib.rs`. +Мы используем [`OnceCell::try_get`] для получения ссылки на инициализированную очередь. Если очередь ещё не инициализирована, мы игнорируем скан-код клавиатуры и печатаем предупреждение. Важно, чтобы мы не пытались инициализировать очередь в этой функции, так как она будет вызываться обработчиком прерываний, который не должен выполнять аллокации в куче. Поскольку эта функция не должна быть доступна из нашего `main.rs`, мы используем видимость `pub(crate)`, чтобы сделать её доступной только для нашего `lib.rs`. [`OnceCell::try_get`]: https://docs.rs/conquer-once/0.2.0/conquer_once/raw/struct.OnceCell.html#method.try_get @@ -1176,7 +1177,7 @@ extern "x86-interrupt" fn keyboard_interrupt_handler( Как и ожидалось, нажатия клавиш больше не выводятся на экран, когда мы запускаем наш проект с помощью `cargo run`. Вместо этого пишется предупреждение, что очередь не инициализирована при каждом нажатия клавиши. -#### Стрим для скан-кодов +#### Стрим Скан-кодов Чтобы инициализировать `SCANCODE_QUEUE` и считывать скан-коды из очереди асинхронным способом, мы создаём новый тип `ScancodeStream`: @@ -1215,7 +1216,7 @@ pub trait Stream { } ``` -Это определение довольно похоже на трейт [`Future`], с следующими отличиями: +Это определение довольно похоже на трейт [`Future`], со следующими отличиями: - Ассоциированный тип называется `Item`, а не `Output`. - Вместо метода `poll`, который возвращает `Poll`, трейт `Stream` определяет метод `poll_next`, который возвращает `Poll>` (обратите внимание на дополнительный `Option`). @@ -1329,7 +1330,7 @@ impl Stream for ScancodeStream { [`AtomicWaker::register`]: https://docs.rs/futures-util/0.3.4/futures_util/task/struct.AtomicWaker.html#method.register [`AtomicWaker::take`]: https://docs.rs/futures/0.3.4/futures/task/struct.AtomicWaker.html#method.take -Обратите внимание, что существует два способа, которыми может произойти уведомление для задачи, которая ещё не вернула `Poll::Pending`. Один из способов — это упомянутое состояние гонки, когда уведомление происходит незадолго до возвращения `Poll::Pending`. Другой способ — это когда очередь больше не пуста после регистрации `waker`, так что возвращается `Poll::Ready`. Поскольку эти ложные уведомления предотвратить невозможно, исполнитель должен уметь правильно с ними справляться. +Обратите внимание, что уведомление для задачи, которая ещё не вернула `Poll::Pending`, может произойти двумя способами. Один из способов — это упомянутое состояние гонки, когда уведомление происходит незадолго до возвращения `Poll::Pending`. Другой способ — это когда очередь больше не пуста после регистрации `waker`, так что возвращается `Poll::Ready`. Поскольку эти ложные уведомления предотвратить невозможно, исполнитель должен уметь правильно с ними справляться. ##### Пробуждение хранящихся Waker @@ -1357,9 +1358,9 @@ pub(crate) fn add_scancode(scancode: u8) { Важно, чтобы мы вызывали `wake` только после добавления в очередь, потому что в противном случае задача может быть разбужена слишком рано, пока очередь всё ещё пуста. Это может, например, произойти при использовании многопоточного исполнителя, который запускает пробуждённую задачу параллельно на другом ядре CPU. Хотя у нас пока нет поддержки потоков, мы добавим её скоро и не хотим, чтобы всё сломалось в этом случае. -#### Задачи Клавиатуры +#### Задачи от Клавиатуры -Теперь, когда мы реализовали трейт `Stream` для нашего `ScancodeStream`, мы можем использовать его для создания асинхронной задачи клавиатуры: +Теперь, когда мы реализовали трейт `Stream` для `ScancodeStream`, мы можем использовать его для создания асинхронной задач от клавиатуры (таски): ```rust // src/task/keyboard.rs @@ -1394,12 +1395,12 @@ pub async fn print_keypresses() { Мы используем `while let` для цикла, пока стрим не вернет `None`, сигнализируя о своем завершении. Поскольку наш метод `poll_next` никогда не возвращает `None`, это фактически бесконечный цикл, поэтому задача `print_keypresses` никогда не завершается. -Давайте добавим задачу `print_keypresses` в наш исполнитель в `main.rs`, чтобы снова получить работающий ввод с клавиатуры: +Давайте добавим таску `print_keypresses` в наш исполнитель в `main.rs`, чтобы снова получить работающий ввод с клавиатуры: ```rust // src/main.rs -use blog_os::task::keyboard; // new +use blog_os::task::keyboard; // новое fn kernel_main(boot_info: &'static BootInfo) -> ! { @@ -1528,7 +1529,7 @@ impl Executor { Чтобы создать `Executor`, мы предоставляем простую функцию `new`. Мы выбираем ёмкость 100 для `task_queue`, что должно быть более чем достаточно на обозримое будущее. В случае, если в нашей системе в какой-то момент будет больше 100 параллельных задач, мы можем легко увеличить этот размер. -#### Cпавн Задач +#### Spawn Задач Как и в `SimpleExecutor`, мы предоставляем метод `spawn` для нашего типа `Executor`, который добавляет данную задачу в дерево `tasks` и немедленно пробуждает её, добавляя её идентификатор в `task_queue`: @@ -1594,9 +1595,9 @@ impl Executor { - Мы используем _деструктуризацию_ [_destructuring_], чтобы разделить `self` на три поля, чтобы избежать некоторых ошибок компилятора. В частности, наша реализация требует доступа к `self.task_queue` изнутри замыкания, что в данный момент пытается полностью заимствовать `self`. Это фундаментальная проблема компилятора, которая будет решена в [RFC 2229], [проблема][RFC 2229 impl]. -- Для каждого извлеченного идентификатора задачи мы получаем мутабельную ссылку на соответствующую задачу из дерева `tasks`. Поскольку наша реализация `ScancodeStream` регистрирует wakers перед проверкой, нужно ли задачу отправить в сон, может случиться так, что произойдёт пробуждение для задачи, которой больше не существует. В этом случае мы просто игнорируем пробуждение и продолжаем с следующим идентификатором из очереди. +- Для каждого извлеченного идентификатора задачи мы получаем мутабельную ссылку на соответствующую задачу из дерева `tasks`. Поскольку наша реализация `ScancodeStream` регистрирует wakers перед проверкой, нужно ли задачу отправить в сон, может случиться так, что произойдёт пробуждение для задачи, которой больше не существует. В этом случае мы просто игнорируем пробуждение и продолжаем со следующим идентификатором из очереди. -- Чтобы избежать накладных расходов на создание waker при каждом опросе, мы используем дерево `waker_cache` для хранения waker для каждой задачи после ее создания. Для этого мы используем метод [`BTreeMap::entry`] в сочетании с [`Entry::or_insert_with`] для создания нового waker, если он ещё не существует, а затем получаем на него мутабельную ссылку. Для создания нового waker мы клонируем `task_queue` и передаём его вместе с идентификатором задачи в функцию `TaskWaker::new` (реализация ниже). Поскольку `task_queue` обёрнута в `Arc`, `clone` только увеличивает счётчик ссылок на значение, но всё равно указывает на ту же выделенную в куче очередь. Обратите внимание, что повторное использование wakers таким образом невозможно для всех реализаций waker, но наш тип `TaskWaker` это позволит. +- Чтобы избежать накладных расходов на создание waker при каждом опросе, мы используем дерево `waker_cache` для хранения waker для каждой задачи после ее создания. Для этого мы используем метод [`BTreeMap::entry`] в сочетании с [`Entry::or_insert_with`] для создания нового waker, если он ещё не существует, а затем получаем на него мутабельную ссылку. Для создания нового waker мы клонируем `task_queue` и передаём его вместе с идентификатором задачи в функцию `TaskWaker::new` (реализация ниже). Поскольку `task_queue` обёрнута в `Arc`, `clone` только увеличивает счётчик ссылок на значение, но всё равно указывает на ту же выделенную в куче очередь. Обратите внимание, что повторное использование wakers таким образом, невозможно для всех реализаций waker, но наш тип `TaskWaker` это позволит. [_destructuring_]: https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#destructuring-to-break-apart-values [RFC 2229]: https://github.com/rust-lang/rfcs/pull/2229 @@ -1640,7 +1641,7 @@ impl TaskWaker { Мы добавляем `task_id` в ссылку на `task_queue`. Поскольку модификации типа [`ArrayQueue`] требуют только совместной ссылки, мы можем реализовать этот метод на `&self`, а не на `&mut self`. -##### The `Wake` Trait +##### Трейт `Wake` Чтобы использовать наш тип `TaskWaker` для опроса futures, нам нужно сначала преобразовать его в экземпляр [`Waker`]. Это необходимо, потому что метод [`Future::poll`] принимает экземпляр [`Context`] в качестве аргумента, который можно создать только из типа `Waker`. Хотя мы могли бы сделать это, предоставив реализацию типа [`RawWaker`], проще и безопаснее реализовать трейт [`Wake`][wake-trait] на основе `Arc` и затем использовать реализации [`From`], предоставленные стандартной библиотекой, для создания `Waker`. @@ -1670,7 +1671,7 @@ impl Wake for TaskWaker { ##### Создание Wakers -Поскольку тип `Waker` поддерживает преобразования [`From`] для всех значений, обёрнутых в `Arc`, которые реализуют трейт `Wake`, мы теперь можем реализовать функцию `TaskWaker::new`, которая требуется для метода `Executor::run_ready_tasks`: +Поскольку тип `Waker` поддерживает преобразования [`From`] для всех значений, обёрнутых в `Arc` и реализующих трейт `Wake`, мы теперь можем реализовать функцию `TaskWaker::new`, необходимую для метода `Executor::run_ready_tasks`: [`From`]: https://doc.rust-lang.org/nightly/core/convert/trait.From.html @@ -1707,7 +1708,7 @@ impl Executor { } ``` -Этот метод просто вызывает функцию `run_ready_tasks` в цикле. Хотя теоретически мы могли бы выйти из функции, когда карта `tasks` станет пустой, этого никогда не произойдёт, так как наша `keyboard_task` никогда не завершается, поэтому простого `loop` будет достаточено. Поскольку функция никогда не возвращается, мы используем тип возвращаемого значения `!`, чтобы пометить функцию как [расходящуюся][diverging] для компилятора. +Этот метод просто вызывает функцию `run_ready_tasks` в цикле. Хотя теоретически мы могли бы выйти из функции, когда карта `tasks` станет пустой, этого никогда не произойдёт, так как наша `keyboard_task` никогда не завершается, поэтому простого `loop` будет достаточно. Поскольку функция никогда не возвращается, мы используем тип возвращаемого значения `!`, чтобы пометить функцию как [расходящуюся][diverging] для компилятора. [diverging]: https://doc.rust-lang.org/stable/rust-by-example/fn/diverging.html @@ -1734,11 +1735,11 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { ![QEMU печатает ".....H...e...l...l..o..... ...a..g..a....i...n...!"](qemu-keyboard-output-again.gif) -Тем не менее загрузка процессора QEMU не уменьшилась. Причина в том, что мы по-прежнему загружаем процессор всё время. Мы больше не опрашиваем задачи, пока они не будут пробуждены снова, но мы всё же проверяем `task_queue` в цикле с занятым ожиданием. Чтобы это исправить, нам нужно перевести процессор в спящий режим, если больше нет работы. +Однако, загрузка процессора QEMU не уменьшилась. Причина в том, что мы по-прежнему загружаем процессор всё время. Мы больше не опрашиваем задачи, пока они не будут пробуждены снова, но мы всё же проверяем `task_queue` в цикле с занятым ожиданием. Чтобы это исправить, нам нужно перевести процессор в спящий режим, если больше нет работы. #### Спать если Idle -Основная идея в том, чтобы выполнять [инструкцию `hlt`][[`hlt` instruction]] при пустой `task_queue`. Эта инструкция ставит процессор в спящий режим до следующего прерывания. Факт, что процессор немедленно активируется снова при возникновении прерывания, обеспечивает возможность прямой реакции, когда обработчик прерываний добавляет задачу в `task_queue`. +Основная идея в том, чтобы выполнять [инструкцию `hlt`][`hlt` instruction] при пустой `task_queue`. Эта инструкция ставит процессор в спящий режим до следующего прерывания. Факт, что процессор немедленно активируется снова при возникновении прерывания, обеспечивает возможность прямой реакции, когда обработчик прерываний добавляет задачу в `task_queue`. [`hlt` instruction]: https://en.wikipedia.org/wiki/HLT_(x86_instruction) @@ -1804,13 +1805,13 @@ impl Executor { Чтобы избежать состояний гонки, мы отключаем прерывания перед проверкой, пуста ли `task_queue`. Если она пуста, мы используем функцию [`enable_and_hlt`], чтобы включить прерывания и поставить процессор в спящий режим в рамках одной атомарной операции. Если очередь больше не пуста, это означает, что прерывание пробудило задачу после возврата `run_ready_tasks`. В этом случае мы снова включаем прерывания и продолжаем выполнение, не выполняя `hlt`. -Теперь наш исполнитель правильно ставит процессор в спящий режим, когда задач нету. Мы можем видеть, что загрузка процессора QEMU значительно снизилась, когда мы снова запускаем наше ядро с помощью `cargo run`. +Теперь наш исполнитель правильно ставит процессор в спящий режим, когда задач нет. Мы можем видеть, что загрузка процессора QEMU значительно снизилась, когда мы снова запускаем наше ядро с помощью `cargo run`. #### Возможные Расширения -Наш исполнитель теперь способен эффективно выполнять задачи. Он использует уведомления waker, чтобы избежать опроса ожидающих задач, и переводит процессор в спящий режим, когда задач нету. Однако наш исполнитель всё ещё довольно примитивный, и существует множество способов расширить его функциональность: +Наш исполнитель теперь способен эффективно выполнять задачи. Он использует уведомления waker, чтобы избежать опроса ожидающих задач, и переводит процессор в спящий режим, когда задач нет. Однако наш исполнитель всё ещё довольно примитивный, и существует множество способов расширить его функциональность: -- **Планирование**: Для нашей `task_queue` мы в настоящее время используем тип [`VecDeque`] для реализации стратегии _первый пришёл — первый вышел_ (FIFO), которая часто также называется _круговым_ планированием. Эта стратегия может быть не самой эффективной для всех нагрузок. Например, имеет смысл приоритизировать задачи с критической задержкой или задачи, выполняющие много ввода-вывода. Для получения дополнительной информации смотрите [главу о планировании][scheduling chapter] книги [_Operating Systems: Three Easy Pieces_] или [статью в Википедии о планировании][scheduling-wiki]. +- **Планирование**: Для нашей `task_queue` мы в настоящее время используем тип [`VecDeque`] для реализации стратегии _первый пришёл — первый вышел_ (FIFO), которая часто также называется _круговым_ планированием. Эта стратегия может быть не самой эффективной для произвольной нагрузки. Например, имеет смысл приоритизировать таски, где критична задержка или таски, выполняющие много ввода-вывода. Для получения дополнительной информации смотрите [главу о планировании][scheduling chapter] книги [_Operating Systems: Three Easy Pieces_] или [статью в Википедии о планировании][scheduling-wiki]. - **Создание задач**: Сейчас метод `Executor::spawn` требует ссылки `&mut self`, и поэтому он недоступен после вызова метода `run`. Чтобы это исправить, мы могли бы создать дополнительный тип `Spawner`, который делит какую-то очередь с исполнителем и позволяет создавать задачи изнутри самих задач. Очередь может быть `task_queue` напрямую или отдельной очередью, которую исполнитель проверяет в своём цикле выполнения. - **Использование потоков**: У нас пока нет поддержки потоков, но мы добавим её в следующем посте. Это сделает возможным запуск нескольких экземпляров исполнителя в разных потоках. Преимущество этого подхода заключается в том, что задержка, вызванная длительными задачами, может быть уменьшена, так как другие задачи могут выполняться параллельно. Этот подход также позволяет использовать несколько ядер процессора. - **Балансировка нагрузки**: При добавлении поддержки потоков становится важно быть в курсе, как распределяются задачи между исполнителями, чтобы обеспечить использование всех ядер процессора. Распространённой техникой для этого является [_work stealing_]. @@ -1822,18 +1823,20 @@ impl Executor { ## Итоги -Мы начали этот пост с введения в **мультизадачность** и различия между _вытесняемой_ мультизадачностью, которая регулярно принудительно прерывает выполняющиеся задачи, и _кооперативной_ мультизадачностью, которая позволяет задачам выполняться до тех пор, пока они добровольно не отдадут управление процессором. +Мы начали этот пост с обсуждения **многозадачности** и различий между _вытесняемой_, которая регулярно прерывает выполняющиеся задачи, и _кооперативной_, позволяющей задачам работать до тех пор, пока они не добровольно отдадут управление процессором. -Затем мы исследовали, как поддержка Rust **async/await** предоставляет реализацию кооперативной мультизадачности на уровне языка. Rust основывает свою реализацию на поллинговом трейте `Future`, который абстрагирует асинхронные задачи. С использованием async/await возможно работать с futures почти так же, как с обычным синхронным кодом. Разница заключается в том, что асинхронные функции снова возвращают `Future`, который в какой-то момент должен быть добавлен в исполнитель для запуска. +Затем мы исследовали, как поддержка Rust **async/await** предоставляет реализацию кооперативной многозадачности на уровне языка. Rust основывает свою реализацию на опросном (polling-based) трейте `Future`, который абстрагирует асинхронные задачи. С использованием async/await возможно работать с futures почти так же, как с обычным синхронным кодом. Разница заключается в том, что асинхронные функции снова возвращают `Future`, который в какой-то момент должен быть добавлен в исполнителя для запуска. За кулисами компилятор преобразует код async/await в _конечный автомат_, при этом каждая операция `.await` соответствует возможной точке остановки. Используя свои знания о программе, компилятор может сохранять только минимальное состояние для каждой точки остановки, что приводит к очень низкому потреблению памяти на задачу. Одной из проблем является то, что сгенерированные автоматы могут содержать _самоссылающиеся_ структуры, например, когда локальные переменные асинхронной функции ссылаются друг на друга. Чтобы избежать недействительных указателей, Rust использует тип `Pin`, чтобы гарантировать, что futures не могут быть перемещены в памяти после их первого опроса. Для нашей **реализации** мы сначала создали очень простой исполнитель, который опрашивает все запущенные задачи в цикле с занятым ожиданием, не используя тип `Waker`. Затем мы продемонстрировали преимущество уведомлений waker, реализовав асинхронную задачу клавиатуры. Задача определяет статический `SCANCODE_QUEUE`, используя неблокирующий тип `ArrayQueue`, предоставленный библиотекой `crossbeam`. Вместо непосредственной обработки нажатий клавиш, обработчик прерываний клавиатуры теперь помещает все полученные скан-коды в очередь и затем пробуждает зарегистрированный `Waker`, чтобы сигнализировать, что новый ввод доступен. На принимающей стороне мы создали тип `ScancodeStream`, чтобы предоставить `Future`, разрешающийся в следующий скан-код в очереди. Это сделало возможным создание асинхронной задачи `print_keypresses`, которая использует async/await для интерпретации и вывода скан-кодов в очереди. -Чтобы использовать уведомления waker для задачи клавиатуры, мы создали новый тип `Executor`, который использует совместно используемую `task_queue` на основе `Arc` для готовых задач. Мы реализовали тип `TaskWaker`, который добавляет идентификаторы разбуженных задач непосредственно в эту `task_queue`, которые затем снова опрашиваются исполнителем. Чтобы сэкономить энергию, когда нет запущенных задач, мы добавили поддержку перевода процессора в спящий режим с использованием инструкции `hlt`. Наконец, мы обсудили некоторые потенциальные расширения для нашего исполнителя, например, предоставление поддержки многопроцессорности. +Чтобы использовать уведомления waker для тасков клавиатуры, мы создали новый тип `Executor`, который использует `task_queue` на основе `Arc` для готовых задач. Мы реализовали тип `TaskWaker`, который добавляет идентификаторы разбуженных задач непосредственно в эту `task_queue`, которые затем снова опрашиваются исполнителем. Чтобы сэкономить энергию, когда нет запущенных задач, мы добавили поддержку перевода процессора в спящий режим с использованием инструкции `hlt`. Наконец, мы обсудили некоторые потенциальные расширения для нашего исполнителя, например, предоставление поддержки мультипроцессинга. + +Для обработки клавиатурных тасков мы использования уведомления о пробуждении (waker notifications). Для этого реализовали новый тип `Executor`, который использует `Arc`-общую `task_queue` для готовых задач. Мы реализовали тип `TaskWaker`, который добавляет идентификаторы разбуженных задач в `task_queue`, которая затем опрашивается исполнителем. Чтобы сэкономить энергию, когда нет запущенных задач, мы добавили поддержку перевода процессора в спящий режим с использованием инструкции `hlt`. ## Что Далее? -Используя async/await, мы теперь имеем базовую поддержку кооперативной мультизадачности в нашем ядре. Хотя кооперативная мультизадачность очень эффективна, она может привести к проблемам с задержкой, когда отдельные задачи выполняются слишком долго, тем самым препятствуя выполнению других задач. По этой причине имеет смысл также добавить поддержку вытесняющей мультизадачности в наше ядро. +Используя async/await, мы теперь имеем базовую поддержку кооперативной многозадачности в нашем ядре. Хотя кооперативная многозадачность очень эффективна, она может привести к проблемам с задержкой, когда отдельные задачи выполняются слишком долго, тем самым препятствуя выполнению других задач. По этой причине имеет смысл также добавить поддержку вытесняющей многозадачности в наше ядро. -В следующем посте мы введём _потоки_ как наиболее распространённую форму вытесняющей мультизадачности. В дополнение к решению проблемы длительных задач, потоки также подготовят нас к использованию нескольких ядер процессора и запуску ненадежных пользовательских программ в будущем. +В следующем посте мы введём _потоки_ как наиболее распространённую форму вытесняющей многозадачности. В дополнение к решению проблемы длительных задач, потоки также подготовят нас к использованию нескольких ядер процессора и запуску ненадежных пользовательских программ в будущем. From 54b8df306ac8fc42f8ef128617a91ff817eb61df Mon Sep 17 00:00:00 2001 From: TakiMoysha <36836047+TakiMoysha@users.noreply.github.com> Date: Tue, 16 Sep 2025 00:05:51 +0200 Subject: [PATCH 8/9] fixed path --- blog/content/edition-2/posts/12-async-await/index.ru.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blog/content/edition-2/posts/12-async-await/index.ru.md b/blog/content/edition-2/posts/12-async-await/index.ru.md index c14f4c87..90f01ad5 100644 --- a/blog/content/edition-2/posts/12-async-await/index.ru.md +++ b/blog/content/edition-2/posts/12-async-await/index.ru.md @@ -1,7 +1,7 @@ +++ title = "Async/Await" weight = 12 -path = "async-await" +path = "ru/async-await" date = 2020-03-27 [extra] From f70d1ffa4b9ebdc9a59e0261b7b96e1cf18f088f Mon Sep 17 00:00:00 2001 From: TakiMoysha <36836047+TakiMoysha@users.noreply.github.com> Date: Tue, 16 Sep 2025 00:14:54 +0200 Subject: [PATCH 9/9] fixed links --- .../edition-2/posts/12-async-await/index.ru.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/blog/content/edition-2/posts/12-async-await/index.ru.md b/blog/content/edition-2/posts/12-async-await/index.ru.md index 90f01ad5..051c9be3 100644 --- a/blog/content/edition-2/posts/12-async-await/index.ru.md +++ b/blog/content/edition-2/posts/12-async-await/index.ru.md @@ -159,7 +159,7 @@ pub enum Poll { Теперь мы знаем, как определяются футуры, и понимаем основную идею метода `poll`. Однако мы все еще не знаем, как эффективно работать с футурами. Проблема в том, что они представляют собой результаты асинхронных задач, которые могут быть еще недоступны. На практике, однако, нам часто нужны эти значения непосредственно для дальнейших вычислений. Поэтому возникает вопрос: как мы можем эффективно получить значение, когда оно нам нужно? -#### Ожидание Futures +#### Ожидание Futures {#waiting-on-futures} Один из возможных ответов — дождаться, пока футура исполнится. Это может выглядеть примерно так: @@ -235,7 +235,7 @@ fn file_len() -> impl Future { [_Futures с нулевой стоимостью в Rust_]: https://aturon.github.io/blog/2016/08/11/futures/ -##### Недостатки +##### Недостатки {#drawbacks} Хотя комбинаторы футур позволяют писать очень эффективный код, их может быть сложно использовать в некоторых ситуациях из-за системы типов и интерфейса на основе замыканий. Например, рассмотрим такой код: @@ -305,7 +305,7 @@ async fn example(min_len: usize) -> String { ([Попробовать](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=d93c28509a1c67661f31ff820281d434)) -Эта функция - прямой перевод `example` написанной [выше](#Недостатки), которая использовала комбинаторы. Используя оператор `.await`, мы можем получить значение футуры без необходимости использования каких-либо замыканий или типов `Either`. В результате мы можем писать наш код так же, как если бы это был обычный синхронный код, с той лишь разницей, что _это все еще асинхронный код_. +Эта функция - прямой перевод `example` написанной [выше](#drawbacks), которая использовала комбинаторы. Используя оператор `.await`, мы можем получить значение футуры без необходимости использования каких-либо замыканий или типов `Either`. В результате мы можем писать наш код так же, как если бы это был обычный синхронный код, с той лишь разницей, что _это все еще асинхронный код_. #### Преобразования Конечных Автоматов @@ -504,7 +504,7 @@ fn example(min_len: usize) -> ExampleStateMachine { Заметьте, что эта функция не запускает автомат. Это фундаментальное архитектурное решение для футур в Rust: они ничего не делают, пока не будет произведена первая проверка на готовность. -#### Закрепление +#### Закрепление {#pinning} Мы уже несколько раз столкнулись с понятием _закрепления_ (pinnig, пиннинг) в этом посте. Наконец, время, чтобы изучить, что такое закрепление и почему оно необходимо. @@ -731,7 +731,7 @@ fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll Используя async/await, можно эргономично работать с футурами в полностью асинхронном режиме. Однако, как мы узнали выше, футуры ничего не делают, пока их не вызовут. Это означает, что нам нужно в какой-то момент вызвать `poll`, иначе асинхронный код никогда не будет выполнен. -Запуская одну футуры, мы можем вручную ожидать ее исполнения в цикле, [как описано выше](#ожидание-futures). Однако этот подход очень неэффективен и непрактичен для программ, создающих большое количество футур. Наиболее распространённым решением этого является создание глобального _исполнителя_ (executor), который отвечает за опрос (polling) всех футур в системе, пока они не завершатся. +Запуская одну футуры, мы можем вручную ожидать ее исполнения в цикле, [как описано выше](#waiting-on-futures). Однако этот подход очень неэффективен и непрактичен для программ, создающих большое количество футур. Наиболее распространённым решением этого является создание глобального _исполнителя_ (executor), который отвечает за опрос (polling) всех футур в системе, пока они не завершатся. #### Исполнитель @@ -830,7 +830,7 @@ pub struct Task { [_trait object_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html [_dynamically dispatched_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html#trait-objects-perform-dynamic-dispatch -[разделе о закреплении]: #Закрепление +[разделе о закреплении]: #pinning Чтобы разрешить создание новых структур `Task` из футур, мы создаём функцию `new`: