mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-16 06:17:49 +00:00
text revisions
This commit is contained in:
@@ -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).
|
||||||
|
|
||||||
<!-- more -->
|
<!-- more -->
|
||||||
|
|
||||||
@@ -28,19 +28,19 @@ translators = ["TakiMoysha"]
|
|||||||
|
|
||||||
## Многозадачность
|
## Многозадачность
|
||||||
|
|
||||||
Одной из основных функций возможностей операционных систем является [_многозадачность_][multitasking], то есть возможность одновременного выполнения нескольких задач. Например, вероятно, пока вы читаете этот пост, у вас открыты другие программы, такие как текстовый редактор или окно терминала. Даже если у вас открыто только одно окно браузера, вероятно, в фоновом режиме выполняются различные задачи по управлению окнами рабочего стола, проверке обновлений или индексированию файлов.
|
Одной из основных функций возможностей операционных систем является [_многозадачность_][multitasking], то есть возможность одновременного выполнения нескольких задач. Например, вероятно, пока вы читаете этот пост, у вас открыты другие программы, вроде текстового редактора или терминала. Даже если у вас открыт только один браузер, вероятно, в фоновом режиме выполняются различные задачи по управлению окнами рабочего стола, проверке обновлений или индексированию файлов.
|
||||||
|
|
||||||
[_multitasking_]: https://en.wikipedia.org/wiki/Computer_multitasking
|
[_multitasking_]: https://en.wikipedia.org/wiki/Computer_multitasking
|
||||||
|
|
||||||
Хотя кажется, что все задачи выполняются параллельно, на одном ядре процессора может выполняться только одна задача за раз. Чтобы создать иллюзию параллельного выполнения задач, операционная система быстро переключается между активными задачами, чтобы каждая из них могла выполнить небольшой прогресс. Поскольку компьютеры работают быстро, мы в большинстве случаев не замечаем этих переключений.
|
Хотя кажется, что все задачи выполняются параллельно, на одном ядре процессора может выполняться только одна задача за раз. Чтобы создать иллюзию параллельного выполнения задач, операционная система быстро переключается между активными задачами, чтобы каждая из них могла выполнить небольшой прогресс. Поскольку компьютеры работают быстро, мы в большинстве случаев не замечаем этих переключений.
|
||||||
|
|
||||||
Когда одноядерные центральные процессоры (ЦП) могут выполнять только одну задачу за раз, многоядерные ЦП могут выполнять несколько задач по настоящему параллельно. Например, процессор с 8 ядрами может выполнять 8 задач одновременно. В следующей статье мы расскажем, как настроить многоядерные ЦП. В этой статье для простоты мы сосредоточимся на одноядерных процессорах. (Стоит отметить, что все многоядерные ЦП запускаются с одним активным ядром, поэтому пока мы можем рассматривать их как одноядерные процессоры).
|
Одноядерные центральные процессоры (ЦП) могут выполнять только одну задачу за раз, а многоядерные ЦП могут выполнять несколько задач по настоящему параллельно. Например, процессор с 8 ядрами может выполнять 8 задач одновременно. В следующей статье мы расскажем, как настроить многоядерные ЦП <!-- hot to setup multi-core CPUs -->. В этой статье для простоты мы сосредоточимся на одноядерных процессорах. (Стоит отметить, что все многоядерные ЦП запускаются с одним активным ядром, поэтому пока мы можем рассматривать их как одноядерные процессоры).
|
||||||
|
|
||||||
Есть две формы многозадачности: _кооперативная_ (совместная) - требует, чтобы задачи регулярно отдавали контроль над процессором для продвижения других задач; _вытесняющая_ (приоритетная) - использующая функционал операционной системы (ОС) для переключения потоков в произвольные моменты моменты времени через принудительную остановку. Далее мы рассмотрим две формы многозадачности более подробно и обсудим их преимущества и недостатки.
|
Есть две формы многозадачности: _кооперативная_ или совместная (_cooperative_) - требует, чтобы задачи регулярно отдавали контроль над процессором для продвижения других задач; _вытесняющая_ или приоритетная () _preemptive_) - использующая функционал операционной системы (ОС) для переключения потоков в произвольные моменты моменты времени через принудительную остановку. Далее мы рассмотрим две формы многозадачности более подробно и обсудим их преимущества и недостатки.
|
||||||
|
|
||||||
### Вытесняющая Многозадачность
|
### Вытесняющая Многозадачность
|
||||||
|
|
||||||
Идея заключается в том, что ОС контролирует, когда переключать задачи. Для этого она использует факт того, что при каждом прирывании она восстанавливает контрлоль над ЦП. Это позволяет переключать задачи всякий раз, когда в системе появляется новый ввод. Например, возможность переключать задачи когда двигается мышка или приходят пакеты по сети. ОС также может определять точное время, в течении которого задаче разрешается выполняться, настроив аппаратный таймер на отправку прерывания по истечению этого времени.
|
Замысел вытесняющей многозадачности в том, что за управление переключением между задачами отвечает ОС. Для этого она использует тот факт, что при каждом прерывании она восстанавливает контроль над ЦП. Это позволяет переключать задачи всякий раз, когда в системе появляется новый ввод. Например, возможность переключать задачи когда двигается мышка или приходят пакеты по сети. ОС также может определять точное время, в течении которого задаче разрешается выполняться, настроив аппаратный таймер на прерывание по истечению этого времени.
|
||||||
|
|
||||||
На следующем рисунку показан процесс переключения задач при аппаратном прерывании:
|
На следующем рисунку показан процесс переключения задач при аппаратном прерывании:
|
||||||
|
|
||||||
@@ -52,52 +52,52 @@ translators = ["TakiMoysha"]
|
|||||||
|
|
||||||
#### Сохранение состояния
|
#### Сохранение состояния
|
||||||
|
|
||||||
Поскольку задачи прерываются в произвольные моменты времени, они могут находиться в середине вычислений. Чтобы иметь возможность возобновить их позже, ОС должна создать копию всего состояния задачи, включая ее [стек вызовов][call stack] и значения всех регистров ЦП. Этот процесс называется [_переключением контекста_][_context switch_].
|
Поскольку задачи прерываются в произвольные моменты времени, они могут находиться в середине вычислений. Чтобы иметь возможность возобновить их позже, ОС должна создать копию всего состояния задачи, включая ее [_стек вызовов_] и значения всех регистров ЦП. Этот процесс называется ["_переключение контекста_"].
|
||||||
|
|
||||||
[call stack]: https://en.wikipedia.org/wiki/Call_stack
|
[_стек вызовов_]: https://ru.wikipedia.org/wiki/Стек_вызовов
|
||||||
[_context switch_]: https://en.wikipedia.org/wiki/Context_switch
|
[_переключение контекста_]: 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) <!-- ?TODO: уточнить перевод -->. Мы подробнее обсудим эти концепции в будущийх постах. Однако сейчас, мы сосредоточимся на кооперативной многозадачности, которая также предоставляет полезные возможности для нашего ядра.
|
Вытесняющая многозадачность и потоки - фундаментальные компонтенты ОС, т.к. они позволяют запускать неизвестные программы в userspace <!-- ?TODO: run untrusted userspace programs -->. Мы подробнее обсудим эти концепции в будущийх постах. Однако сейчас, мы сосредоточимся на кооперативной многозадачности, которая также предоставляет полезные возможности для нашего ядра.
|
||||||
|
|
||||||
### Кооперативная Многозадачность
|
### Кооперативная Многозадачность
|
||||||
|
|
||||||
Вместо принудительной остановки выполняющихся задач в произвольные моменты времени, кооперативная многозадачность позволяет каждой задаче выполняться до тех пор, пока она добровольно не уступит контроль над ЦП. Это позволяет задачам самостоятельно приостанавливаться в удобные моменты времени, например, когда им нужно ждать операции ввода-вывода.
|
Вместо принудительной остановки выполняющихся задач в произвольные моменты времени, кооперативная многозадачность позволяет каждой задаче выполняться до тех пор, пока она добровольно не уступит контроль над ЦП. Это позволяет задачам самостоятельно приостанавливаться в удобные моменты времени, например, когда им нужно ждать операции ввода-вывода.
|
||||||
|
|
||||||
Кооперативная многозадачность часто используется на языковом уровне, например в виде [сопрограмм][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
|
[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)
|
[_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 сохраняет все локальные переменные, которые еще нужны, в автоматически сгенерированной структуре (см. ниже). Благодаря резервному копированию соответствующих частей стека вызовов перед приостановкой все задачи могут использовать один стек вызовов, что приводит к значительному снижению потребления памяти на задачу. Это позволяет создавать практически любое количество кооперативных задач без исчерпания памяти.
|
||||||
|
|
||||||
#### Обсуждение
|
#### Обсуждение
|
||||||
|
|
||||||
Недостатком кооперативной многозадачности является то, что некооперативная задача может потенциально выполняться в течение неограниченного времени. Таким образом, вредоносная или содержащая ошибки задача может помешать выполнению других задач и замедлить или даже заблокировать работу всей системы. По этой причине кооперативная многозадачность должна использоваться только в том случае, если известно, что все задачи будут взаимодействовать друг с другом. В качестве противоположного примера можно привести то, что не стоит полагаться на взаимодействие произвольных программ пользовательского уровня в операционной системе.
|
Недостатком кооперативной многозадачности является то, что некооперативная задача может потенциально выполняться в течение неограниченного времени. Таким образом, вредоносная или содержащая ошибки задача может помешать выполнению других задач и замедлить или даже заблокировать работу всей системы. По этой причине кооперативная многозадачность должна использоваться только в том случае если известно, что все задачи будут взаимодействовать друг с другом (<!-- ?TODO: when all tasks are known to cooperate -->). Как контрпример, не стоит полагаться на взаимодействие произвольных программ пользовательского пространства (user-level) в ОС.
|
||||||
|
|
||||||
Однако высокая производительность и преимущества кооперативной многозадачности в плане памяти делают ее хорошим подходом для использования внутри программы, особенно в сочетании с асинхронными операциями. Поскольку ядро операционной системы является программой, критичной с точки зрения производительности, которая взаимодействует с асинхронным оборудованием, кооперативная многозадачность кажется хорошим подходом для реализации параллелизма.
|
Однако высокая производительность и преимущества кооперативной многозадачности в плане памяти делают ее хорошим подходом для использования внутри программы, особенно в сочетании с асинхронными операциями. Поскольку ядро операционной системы является программой, критичной с точки зрения производительности, которая взаимодействует с асинхронным оборудованием, кооперативная многозадачность кажется хорошим подходом для реализации параллелизма.
|
||||||
|
|
||||||
## Async/Await в Rust
|
## Async/Await в Rust
|
||||||
|
|
||||||
Rust предоставляет отличную поддержку кооперативной многозадачности в виде async/await. Прежде чем мы сможем изучить, что такое async/await и как оно работает, нам необходимо понять, как работают _futures_ и асинхронное программирование в Rust.
|
Rust предоставляет отличную поддержку кооперативной многозадачности в виде async/await. Прежде чем мы сможем изучить, что такое async/await и как оно работает, нам необходимо понять, как работают _futures_ (футуры) и асинхронное программирование в Rust.
|
||||||
|
|
||||||
### Futures
|
### Futures
|
||||||
|
|
||||||
@@ -105,9 +105,9 @@ _Future_ представляет значение, которое может б
|
|||||||
|
|
||||||
#### Пример
|
#### Пример
|
||||||
|
|
||||||
Концепцию future лучше всего проиллюстрировать небольшим примером:
|
Концепцию футур лучше всего проиллюстрировать небольшим примером:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Эта диаграмма последовательности показывает функцию `main`, которая считывает файл из файловой системы, а затем вызывает функцию `foo`. Этот процесс повторяется дважды: один раз с синхронным вызовом `read_file` и один раз с асинхронным вызовом `async_read_file`.
|
Эта диаграмма последовательности показывает функцию `main`, которая считывает файл из файловой системы, а затем вызывает функцию `foo`. Этот процесс повторяется дважды: один раз с синхронным вызовом `read_file` и один раз с асинхронным вызовом `async_read_file`.
|
||||||
|
|
||||||
@@ -144,9 +144,9 @@ pub enum Poll<T> {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Когда значение уже доступно (например, файл был полностью прочитан с диска), оно возвращается, обернутое в вариант `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
|
[_pinned_]: https://doc.rust-lang.org/nightly/core/pin/index.html
|
||||||
|
|
||||||
@@ -154,11 +154,11 @@ pub enum Poll<T> {
|
|||||||
|
|
||||||
[`Waker`]: https://doc.rust-lang.org/nightly/core/task/struct.Waker.html
|
[`Waker`]: https://doc.rust-lang.org/nightly/core/task/struct.Waker.html
|
||||||
|
|
||||||
### Working with Futures
|
### Работа с Futures
|
||||||
|
|
||||||
Теперь мы знаем, как определяются футуры, и понимаем основную идею метода `poll`. Однако мы все еще не знаем, как эффективно работать с футурами. Проблема в том, что они представляют собой результаты асинхронных задач, которые могут быть еще недоступны. На практике, однако, нам часто нужны эти значения непосредственно для дальнейших вычислений. Поэтому возникает вопрос: как мы можем эффективно получить значение, когда оно нам нужно?
|
Теперь мы знаем, как определяются футуры, и понимаем основную идею метода `poll`. Однако мы все еще не знаем, как эффективно работать с футурами. Проблема в том, что они представляют собой результаты асинхронных задач, которые могут быть еще недоступны. На практике, однако, нам часто нужны эти значения непосредственно для дальнейших вычислений. Поэтому возникает вопрос: как мы можем эффективно получить значение, когда оно нам нужно?
|
||||||
|
|
||||||
#### Waiting on Futures
|
#### Ожидание Futures
|
||||||
|
|
||||||
Один из возможных ответов — дождаться, пока футура исполнится. Это может выглядеть примерно так:
|
Один из возможных ответов — дождаться, пока футура исполнится. Это может выглядеть примерно так:
|
||||||
|
|
||||||
@@ -177,7 +177,8 @@ let file_content = loop {
|
|||||||
Более эффективным подходом может быть _блокировка_ текущего потока до тех пор, пока футура не станет доступной. Конечно, это возможно только при наличии потоков, поэтому это решение не работает для нашего ядра, по крайней мере, пока. Даже в системах, где поддерживается блокировка, она часто нежелательна, поскольку превращает асинхронную задачу в синхронную, тем самым сдерживая потенциальные преимущества параллельных задач в плане производительности.
|
Более эффективным подходом может быть _блокировка_ текущего потока до тех пор, пока футура не станет доступной. Конечно, это возможно только при наличии потоков, поэтому это решение не работает для нашего ядра, по крайней мере, пока. Даже в системах, где поддерживается блокировка, она часто нежелательна, поскольку превращает асинхронную задачу в синхронную, тем самым сдерживая потенциальные преимущества параллельных задач в плане производительности.
|
||||||
|
|
||||||
#### Комбинаторы Future
|
#### Комбинаторы 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
|
[`Iterator`]: https://doc.rust-lang.org/stable/core/iter/trait.Iterator.html
|
||||||
|
|
||||||
@@ -214,23 +215,29 @@ fn file_len() -> impl Future<Output = usize> {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Этот код не совсем корректен, потому что не учитывает [_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
|
[_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`], которые позволяют манипулировать результатом с помощью произвольных замыканий.
|
Так как ручное написание функций-комбинаторов сложно, они обычно предоставляются библиотеками. Стандартная библиотека 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/
|
[_Futures с нулевой стоимостью в Rust_]: https://aturon.github.io/blog/2016/08/11/futures/
|
||||||
|
|
||||||
##### Недостатки
|
##### Недостатки
|
||||||
|
|
||||||
Хотя future комбинаторы позволяют писать очень эффективный код, их может быть сложно использовать в некоторых ситуациях из-за системы типов и интерфейса на основе замыканий. Например, рассмотрим такой код:
|
Хотя комбинаторы футур позволяют писать очень эффективный код, их может быть сложно использовать в некоторых ситуациях из-за системы типов и интерфейса на основе замыканий. Например, рассмотрим такой код:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn example(min_len: usize) -> impl Future<Output = String> {
|
fn example(min_len: usize) -> impl Future<Output = String> {
|
||||||
async_read_file("foo.txt").then(move |content| {
|
async_read_file("foo.txt").then(move |content| {
|
||||||
@@ -243,12 +250,11 @@ fn example(min_len: usize) -> impl Future<Output = String> {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
([Попробовать](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`.
|
Здесь мы читаем файл `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
|
[`move` keyword]: https://doc.rust-lang.org/std/keyword.move.html
|
||||||
[`Either`]: https://docs.rs/futures/0.3.4/futures/future/enum.Either.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
|
[_state machine_]: https://en.wikipedia.org/wiki/Finite-state_machine
|
||||||
|
|
||||||
@@ -314,7 +320,7 @@ async fn example(min_len: usize) -> String {
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Диаграмма использует стрелки для представления переключений состояний и ромбы для представления альтернативных путей. Например, если файл `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` машина состояний начинает с последнего состояния ожидания и повторяет последнюю операцию.
|
Мы видим, что первый вызов `poll` запускает функцию и она выполняться до тех пор, пока у футуры не будет результата. Если все футуры на пути готовы, ф-ция может выполниться до состояния _"end"_ , то есть вернуть свой результат, завернутый в `Poll::Ready`. В противном случае конечный автомат переходит в состояние ожидания и возвращает `Poll::Pending`. При следующем вызове `poll` машина состояний начинает с последнего состояния ожидания и повторяет последнюю операцию.
|
||||||
|
|
||||||
@@ -323,6 +329,7 @@ async fn example(min_len: usize) -> String {
|
|||||||
Для продолжнеия работы с последнего состояния ожидания, автомат должен отслеживать текущее состояние внутри себя. Еще, он должен сохранять все переменные, которые необходимы для продолжнеия выполнения при следующем вызове `poll`. Здесь компилятор действительно может проявить себя: зная, когда используются те или иные переменные, он может автоматически создавать структуры с точным набором требуемых переменных.
|
Для продолжнеия работы с последнего состояния ожидания, автомат должен отслеживать текущее состояние внутри себя. Еще, он должен сохранять все переменные, которые необходимы для продолжнеия выполнения при следующем вызове `poll`. Здесь компилятор действительно может проявить себя: зная, когда используются те или иные переменные, он может автоматически создавать структуры с точным набором требуемых переменных.
|
||||||
|
|
||||||
Например, компилятор генерирует структуры для вышеприведенной ф-ции `example`:
|
Например, компилятор генерирует структуры для вышеприведенной ф-ции `example`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// снова `example` что бы вам не пришлось прокручивать вверх
|
// снова `example` что бы вам не пришлось прокручивать вверх
|
||||||
async fn example(min_len: usize) -> String {
|
async fn example(min_len: usize) -> String {
|
||||||
@@ -353,7 +360,7 @@ struct WaitingOnBarTxtState {
|
|||||||
struct EndState {}
|
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"_, в структуре ничего нет, т.к. ф-ция завершилась полностью.
|
Состояние "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
|
```rust
|
||||||
impl Future for ExampleStateMachine {
|
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
|
```rust
|
||||||
ExampleStateMachine::Start(state) => {
|
ExampleStateMachine::Start(state) => {
|
||||||
@@ -411,9 +418,10 @@ ExampleStateMachine::Start(state) => {
|
|||||||
*self = ExampleStateMachine::WaitingOnFooTxt(state);
|
*self = ExampleStateMachine::WaitingOnFooTxt(state);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
Машина состояний находится в состоянии `Start`, когда она прямо в начале функции. В этом случае выполняем весь код из тела функции `example` до первого `.await`. Чтобы обработать операцию `.await`, мы меняем состояние машины на `WaitingOnFooTxt`, которое включает в себя построение структуры `WaitingOnFooTxtState`.
|
|
||||||
|
|
||||||
Пока `match self {…}` выполняется в цилке, выполнение прыгает к `WaitingOnFooTxt`:
|
Автомат находится в состоянии `Start`, когда ф-ция только начинает выполнение. В этом случае выполняем весь код из тела функции `example` до первого `.await`. Чтобы обработать операцию `.await`, мы меняем состояние на `WaitingOnFooTxt`, которое включает в себя построение структуры `WaitingOnFooTxtState`.
|
||||||
|
|
||||||
|
Пока `match self {…}` выполняется в цикле, выполнение переходит к `WaitingOnFooTxt`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
ExampleStateMachine::WaitingOnFooTxt(state) => {
|
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`.
|
В случае входа в ветку `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` состояния выглядит так:
|
В итоге, код для `End` состояния выглядит так:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
ExampleStateMachine::End(_) => {
|
ExampleStateMachine::End(_) => {
|
||||||
panic!("poll вызван после возврата Poll::Ready");
|
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
|
[_корутинах_]: https://doc.rust-lang.org/stable/unstable-book/language-features/coroutines.html
|
||||||
|
|
||||||
Последняя часть загадки – сгенерированный код для самой функции `example`. Помните, что заголовок функции был определён следующим образом:
|
Последняя часть пазла – сгенерированный код для самой функции `example`. Помните, что заголовок функции был определён следующим образом:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
async fn example(min_len: usize) -> String
|
async fn example(min_len: usize) -> String
|
||||||
```
|
```
|
||||||
|
|
||||||
Теперь, когда весь функционал реализуется машиной состояний, единственное, что ф-ция должна сделать - это инициализировать эту машику и вернуть ее. Сгенерированный код для этого может выглядеть следующим образом:
|
Теперь, когда весь функционал реализуется конечным автоматом, единственное, что ф-ция должна сделать - это инициализировать этот автомат и вернуть его. Сгенерированный код для может выглядеть так:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn example(min_len: usize) -> ExampleStateMachine {
|
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, пиннинг) в этом посте. Наконец, время чтобы изучить, что такое закрепление и почему оно необходимо.
|
Мы уже несколько раз столкнулись с понятием _закрепления_ (pinnig, пиннинг) в этом посте. Наконец, время чтобы изучить, что такое закрепление и почему оно необходимо.
|
||||||
|
|
||||||
> [!note] pinning - механизм, который гарантирует, что объект в памяти не будет перемещен.
|
|
||||||
|
|
||||||
#### Самоссылающиеся структуры
|
#### Самоссылающиеся структуры
|
||||||
|
|
||||||
Как объяснялось выше, переходы конечных автоматов хранят локальные переменные для каждой точки остановки в структуре. Для простых примеров, как наш `example` функции, это было просто и не привело к никаким проблемам. Однако делаются сложнее, когда переменные ссылаются друг на друга. Например, рассмотрите следующую функцию:
|
Как объяснялось выше, переходы конечных автоматов хранят локальные переменные для каждой точки остановки в структуре. Для простых примеров, как наш `example` функции, это было просто и не привело к никаким проблемам. Однако делаются сложнее, когда переменные ссылаются друг на друга. Например, рассмотрим код:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
async fn pin_example() -> i32 {
|
async fn pin_example() -> i32 {
|
||||||
@@ -542,18 +549,19 @@ struct WaitingOnWriteState {
|
|||||||
|
|
||||||
Существует три основных подхода к решению проблемы висящих указателей (dangling pointers):
|
Существует три основных подхода к решению проблемы висящих указателей (dangling pointers):
|
||||||
|
|
||||||
- **Обновление указателя при перемещении**: Идея состоит в обновлении внутреннего указателя при каждом перемещении структуры в памяти, чтобы она оставалась действительной после перемещения. Однако этот подход требует значительных изменений в Rust, которые могут привести к потенциальным значительным потерям производительности. Причина заключается в том, что необходимо каким-то образом отслеживать тип всех полей структуры и проверять на каждом операции перемещения, требуется ли обновление указателя.
|
- **Обновление указателя при перемещении**: Суть в обновлении внутреннего указателя при каждом перемещении структуры в памяти, чтобы она оставалась действительной после перемещения. Однако этот подход требует значительных изменений в Rust, которые могут привести к потенциальным значительным потерям производительности. Причина заключается в том, что необходимо каким-то образом отслеживать тип всех полей структуры и проверять на каждой операции перемещения, требуется ли обновление указателя.
|
||||||
- **Хранение смещения (offset) вместо самоссылающихся ссылок**: Чтобы избежать необходимости обновления указателей, компилятор мог бы попытаться хранить самоссы ссылки в форме смещений от начала структуры вместо прямых ссылок. Например, поле `element` вышеупомянутой `WaitingOnWriteState` структуры можно было бы хранить в виде поля `element_offset` c значением 8, потому что элемент массива, на который указывает ссылка, находится за 8 байтов после начала структуры. Смещение остается неизменным при перемещении структуры, так что не требуются обновления полей.
|
- **Хранение смещения (offset) вместо самоссылающихся ссылок**: Чтобы избежать необходимости обновления указателей, компилятор мог бы попытаться хранить саммоссылки в форме смещений от начала структуры вместо прямых ссылок. Например, поле `element` вышеупомянутой `WaitingOnWriteState` структуры можно было бы хранить в виде поля `element_offset` c значением 8, потому что элемент массива, на который указывает ссылка, находится за 8 байтов после начала структуры. Смещение остается неизменным при перемещении структуры, так что не требуются обновления полей.
|
||||||
Проблема с этим подходом в том, что требуется, чтобы компилятор обнаружил всех самоссылок. Это невозможно на этапе компилящии потому, что значение ссылки может зависеть от ввода пользователя, так что нам потребуется система анализа ссылок и корректная генерация состояния для структур во время исполнения. Это приведёт к дополнительным расходам времени на выполнение, а также предотвратит определённые оптимизации компилятора, что приведёт к еще большим потерям производительности.
|
|
||||||
- **Запретить перемещать структуру**: Мы увидели выше, что висящий указатель возникает только при перемещении структуры в памяти. Запретив все операции перемещения для самоссылающихся структур, можно избежать этой проблемы. Большое преимущество этого подхода состоит в том, что он можно реализовать на уровне системы типов без дополнительных расходов времени выполнения. Недостаток заключается в том, что оно возлагает на программиста обязанности по обработке перемещений самоссылающихся структур.
|
|
||||||
|
|
||||||
Rust выбрал третий подход из-за принципа предоставления _бесплатных абстракций_ (zero cost abstractions), что означает, что абстракции не должны накладывать дополнительные расходы времени выполнения. API [_pinning_] предлагалось для решения этой проблемы в RFC 2349 (<https://github.com/rust-lang/rfcs/blob/master/text/2349-pin.md>). В следующем разделе мы дадим краткий обзор этого API и объясним, как оно работает с async/await и futures.
|
Проблема с этим подходом в том, что он требует от компилятора обнаружения всех самоссылок. Это невозможно на этапе компилящии, т.к. значения ссылки может зависеть от ввода пользователя, так что нам потребуется система анализа ссылок и корректная генерация состояния для структур во время исполнения. Это накладывает расходы на время выполнения и предотвратит определённые оптимизации компилятора, что приведёт к еще большим потерям производительности.
|
||||||
|
- **Запретить перемещать структуру**: Мы увидели выше, что висящий указатель возникает только при перемещении структуры в памяти. Запретив все операции перемещения для самоссылающихся структур, можно избежать этой проблемы. Большое преимущество том, что это можно реализовать на уровне системы типов без расходов к времени исполнения. Недостаток в том, что оно возлагает на программиста обязанности по обработке перемещений самоссылающихся структур.
|
||||||
|
|
||||||
#### Значения на Куче (Heap)
|
Rust выбрал третий подход из-за принципа предоставления _бесплатных абстракций_ (zero-cost abstractions), что означает, что абстракции не должны накладывать дополнительные расходы времени выполнения. API [_pinning_] предлагалось для решения этой проблемы в RFC 2349 (<https://github.com/rust-lang/rfcs/blob/master/text/2349-pin.md>). В следующем разделе мы дадим краткий обзор этого API и объясним, как оно работает с async/await и futures.
|
||||||
|
|
||||||
Первый наблюдение состоит в том, что значения, выделенные на [куче], обычно имеют фиксированный адрес памяти. Они создаются с помощью вызова `allocate` и затем ссылаются на тип указателя, такой как `Box<T>`. Хотя перемещение указательного типа возможно, значение кучи, которое указывает на него, остается в том же адресе памяти до тех пор, пока оно не будет освобождено с помощью вызова `deallocate` еще раз.
|
#### Значения в Куче (Heap)
|
||||||
|
|
||||||
[heap-allocated]: @/edition-2/posts/10-heap-allocation/index.md
|
Первый наблюдение состоит в том, что значения [аллоцированные в куче], обычно имеют фиксированный адрес памяти. Они создаются с помощью вызова `allocate` и затем ссылаются на тип указателя, такой как `Box<T>`. Хотя перемещение указательного типа возможно, значение кучи, которое указывает на него, остается в том же адресе памяти до тех пор, пока оно не будет освобождено с помощью вызова `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
|
[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` остается действительным даже при перемещении указателя.
|
Когда мы запускаем этот код в [песочнице][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);
|
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`], чтобы заменить значение, выделенное в куче, новым экземпляром структуры. Это позволяет нам переместить исходное значение `heap_value` в стек, в то время как поле `self_ptr` структуры теперь является висящим указателем, который по-прежнему указывает на старый адрес в куче. Когда вы запустите пример в песочнице, вы увидите, что строки _«value at:»_ и _«internal reference:»_, показывают разные указатели. Таким образом, выделение значения в куче недостаточно для обеспечения безопасности самоссылок.
|
||||||
|
|
||||||
[`mem::replace`]: https://doc.rust-lang.org/nightly/core/mem/fn.replace.html
|
[`mem::replace`]: https://doc.rust-lang.org/nightly/core/mem/fn.replace.html
|
||||||
|
|
||||||
Основная проблема, которая привела к вышеуказанной ошибке, заключается в том, что `Box<T>` позволяет нам получить ссылку `&mut T` на значение, выделенное в куче. Эта ссылка `&mut` позволяет использовать такие методы, как [`mem::replace`] или [`mem::swap`], для аннулирования значения, выделенного в куче. Чтобы решить эту проблему, мы должны предотвратить создание ссылок `&mut` на самореференциальные структуры.
|
Основная проблема, которая привела к вышеуказанной ошибке, заключается в том, что `Box<T>` позволяет нам получить ссылку `&mut T` на значение, выделенное в куче. Эта ссылка `&mut` позволяет использовать такие методы, как [`mem::replace`] или [`mem::swap`], для аннулирования значения, выделенного в куче. Чтобы решить эту проблему, мы должны предотвратить создание ссылок `&mut` на самоссылающиеся структуры.
|
||||||
|
|
||||||
[`mem::swap`]: https://doc.rust-lang.org/nightly/core/mem/fn.swap.html
|
[`mem::swap`]: https://doc.rust-lang.org/nightly/core/mem/fn.swap.html
|
||||||
|
|
||||||
#### `Pin<Box<T>>` и `Unpin`
|
#### `Pin<Box<T>>` и `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<Box<T>>` для них. В результате их внутренние самореференции гарантированно остаются действительными.
|
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<Box<T>>` для них. В результате их внутренние самоссылки гарантированно остаются действительными.
|
||||||
|
|
||||||
[`Pin`]: https://doc.rust-lang.org/stable/core/pin/struct.Pin.html
|
[`Pin`]: https://doc.rust-lang.org/stable/core/pin/struct.Pin.html
|
||||||
[`Unpin`]: https://doc.rust-lang.org/nightly/std/marker/trait.Unpin.html
|
[`Unpin`]: https://doc.rust-lang.org/nightly/std/marker/trait.Unpin.html
|
||||||
@@ -682,11 +690,11 @@ unsafe {
|
|||||||
|
|
||||||
Теперь единственной оставшейся ошибкой является желаемая ошибка на `mem::replace`. Помните, что эта операция пытается переместить значение, размещённое в куче, на стек, что нарушило бы самоссылку, хранящуюся в поле `self_ptr`. Отказываясь от `Unpin` и используя `Pin<Box<T>>`, мы можем предотвратить эту операцию на этапе компиляции и таким образом безопасно работать с самоссыльными структурами. Как мы видели, компилятор не может доказать, что создание самоссылки безопасно (пока), поэтому нам нужно использовать небезопасный блок и самостоятельно проверить корректность.
|
Теперь единственной оставшейся ошибкой является желаемая ошибка на `mem::replace`. Помните, что эта операция пытается переместить значение, размещённое в куче, на стек, что нарушило бы самоссылку, хранящуюся в поле `self_ptr`. Отказываясь от `Unpin` и используя `Pin<Box<T>>`, мы можем предотвратить эту операцию на этапе компиляции и таким образом безопасно работать с самоссыльными структурами. Как мы видели, компилятор не может доказать, что создание самоссылки безопасно (пока), поэтому нам нужно использовать небезопасный блок и самостоятельно проверить корректность.
|
||||||
|
|
||||||
#### Пиннинг на стеке и `Pin<&mut T>`
|
#### Закрепление в стеке и `Pin<&mut T>`
|
||||||
|
|
||||||
В предыдущем разделе мы узнали, как использовать `Pin<Box<T>>` для безопасного создания самоссыльного значения, размещённого в куче. Хотя этот подход работает хорошо и относительно безопасен (кроме unsafe), необходимая аллокация в куче бьет по производительности. Поскольку Rust стремится предоставлять _абстракции с нулевыми затратами_ (_zero-cost abstractions_) где это возможно, API закрепления также позволяет создавать экземпляры `Pin<&mut T>`, которые указывают на значения, размещённые на стеке.
|
В предыдущем разделе мы узнали, как использовать `Pin<Box<T>>` для безопасного создания самоссыльного значения, размещённого в куче. Хотя этот подход работает хорошо и относительно безопасен (кроме unsafe), необходимая аллокация в куче бьет по производительности. Поскольку Rust стремится предоставлять _абстракции с нулевыми затратами_ (_zero-cost abstractions_) где это возможно, API закрепления также позволяет создавать экземпляры `Pin<&mut T>`, которые указывают на значения, размещённые в стеке.
|
||||||
|
|
||||||
В отличие от экземпляров `Pin<Box<T>>`, которые имеют _владение_ обёрнутым значением, экземпляры `Pin<&mut T>` лишь временно заимствуют обёрнутое значение. Это усложняет задачу, так как программисту необходимо самостоятельно обеспечивать дополнительные гарантии. Важно, чтобы `Pin<&mut T>` оставался закрепленным на протяжении всей жизни ссылочного `T`, что может быть сложно проверить для переменных на стеке. Чтобы помочь с этим, существуют такие крейты, как [`pin-utils`], но я все же не рекомендую закреплять на стеке, если вы не уверены в своих действиях.
|
В отличие от экземпляров `Pin<Box<T>>`, которые _владеют_ (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/
|
[`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` 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
|
[`Pin::new_unchecked`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.new_unchecked
|
||||||
|
|
||||||
#### Пиннинг и Футуры
|
#### Закрепление и Футуры
|
||||||
|
|
||||||
Как мы уже увидели в этом посте, метод [`Future::poll`] использует пиннинг в виде параметра `Pin<&mut Self>`:
|
Как мы уже увидели в этом посте, метод [`Future::poll`] использует пиннинг в виде параметра `Pin<&mut Self>`:
|
||||||
|
|
||||||
@@ -709,7 +717,7 @@ fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>
|
|||||||
|
|
||||||
[self-ref-async-await]: @/edition-2/posts/12-async-await/index.md#self-referential-structs
|
[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/
|
[`futures`]: https://docs.rs/futures/0.3.4/futures/
|
||||||
|
|
||||||
@@ -718,25 +726,23 @@ fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>
|
|||||||
[map-src]: https://docs.rs/futures-util/0.3.4/src/futures_util/future/future/map.rs.html
|
[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
|
[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`, иначе асинхронный код никогда не будет выполнен.
|
Используя async/await, можно эргономично работать с футурами в полностью асинхронном режиме. Однако, как мы узнали выше, футуры ничего не делают, пока их не вызовут. Это означает, что нам нужно в какой-то момент вызвать `poll`, иначе асинхронный код никогда не будет выполнен.
|
||||||
|
|
||||||
Запуская одну футуры, мы можем вручную ожидать ее исполнения в цикле, [как описано выше](#waiting-on-futures). Однако этот подход очень неэффективен и непрактичен для программ, создающих большое количество футур. Наиболее распространённым решением этой проблемы является определение глобального _исполнителя_, который отвечает за опрос всех футур в системе, пока они не завершатся.
|
Запуская одну футуры, мы можем вручную ожидать ее исполнения в цикле, [как описано выше](#ожидание-futures). Однако этот подход очень неэффективен и непрактичен для программ, создающих большое количество футур. Наиболее распространённым решением этого является создание глобального _исполнителя_ (executor), который отвечает за опрос (polling) всех футур в системе, пока они не завершатся.
|
||||||
|
|
||||||
#### Executors
|
#### Исполнитель
|
||||||
|
|
||||||
Цель исполнителя в том, чтобы позволить создавать футуры в качестве независимых задач, обычно через какой-либо метод `spawn`. Исполнитель затем отвечает за опрос всех футур, пока они не завершатся. Большое преимущество управления всеми футурами в одном месте состоит в том, что исполнитель может переключаться на другую футуру, когда текущая футура возвращает `Poll::Pending`. Таким образом, асинхронные операции выполняются параллельно, и процессор остаётся загруженным.
|
Цель исполнителя в том, чтобы позволить создавать футуры в качестве независимых задач, обычно через какой-либо метод `spawn`. Исполнитель затем отвечает за опрос всех футур, пока они не завершатся. Большое преимущество управления всеми футурами в одном месте состоит в том, что исполнитель может переключаться на другую футуру, когда текущая футура возвращает `Poll::Pending`. Таким образом, асинхронные операции выполняются параллельно, и процессор остаётся загруженным.
|
||||||
|
|
||||||
Многие реализации исполнителей также могут использовать преимущества систем с несколькими ядрами процессора. Они создают [thread pool], способный использовать все ядра, если достаточно работы, и применяют такие техники, как [work stealing], для балансировки нагрузки между ядрами. Существуют также специальные реализации исполнителей для встроенных систем, которые оптимизируют низкую задержку и затраты памяти.
|
Многие реализации исполнителей также могут использовать преимущества систем с мультиядерными процессорами. Они создают [пул потоков][thread pool], способный использовать все ядра, если достаточно работы, и применяют такие техники, как [work stealing], для балансировки нагрузки между ядрами. Существуют также специальные реализации исполнителей для встроенных систем, которые оптимизируют низкую задержку и затраты памяти.
|
||||||
|
|
||||||
[thread pool]: https://en.wikipedia.org/wiki/Thread_pool
|
[thread pool]: https://en.wikipedia.org/wiki/Thread_pool
|
||||||
[work stealing]: https://en.wikipedia.org/wiki/Work_stealing
|
[work stealing]: https://en.wikipedia.org/wiki/Work_stealing
|
||||||
|
|
||||||
Чтобы избежать накладных расходов на повторный опрос футур, исполнители обычно используют API _waker_, поддерживаемый футурами Rust.
|
Чтобы избежать накладных расходов на повторный опрос футур, исполнители обычно используют API _waker_, поддерживаемый футурами Rust.
|
||||||
|
|
||||||
<!-- !TODO: added wakers to post -->
|
|
||||||
|
|
||||||
#### Wakers
|
#### Wakers
|
||||||
|
|
||||||
Идея API waker в том, что специальный тип [`Waker`] передаётся в каждом вызове `poll`, при этом обернутый в тип [`Context`]. Этот тип `Waker` создаётся исполнителем и может использоваться асинхронной задачей для сигнализации о своём (частичном) завершении. В результате исполнитель не должен вызывать `poll` на футуре, которая ранее вернула `Poll::Pending`, пока не получит уведомление от соответствующего waker.
|
Идея API waker в том, что специальный тип [`Waker`] передаётся в каждом вызове `poll`, при этом обернутый в тип [`Context`]. Этот тип `Waker` создаётся исполнителем и может использоваться асинхронной задачей для сигнализации о своём (частичном) завершении. В результате исполнитель не должен вызывать `poll` на футуре, которая ранее вернула `Poll::Pending`, пока не получит уведомление от соответствующего waker.
|
||||||
@@ -755,30 +761,30 @@ async fn write_file() {
|
|||||||
|
|
||||||
Мы увидим, как работает тип `Waker` в деталях, когда создадим свой собственный исполнитель с поддержкой waker в разделе реализации этого поста.
|
Мы увидим, как работает тип `Waker` в деталях, когда создадим свой собственный исполнитель с поддержкой waker в разделе реализации этого поста.
|
||||||
|
|
||||||
### Cooperative Multitasking?
|
### Кооперативная Мультизадачность?
|
||||||
|
|
||||||
В начале этого поста мы говорили о вытесняющей (preemptive) и кооперативной многозадачности. В то время как вытесняющая многозадачность полагается на операционную систему для принудительного переключения между выполняемыми задачами, кооперативная многозадачность требует, чтобы задачи добровольно уступали контроль над CPU через операцию _yield_ на регулярной основе. Большое преимущество кооперативного подхода в том, что задачи могут сохранять своё состояние самостоятельно, что приводит к более эффективным переключениям контекста и делает возможным совместное использование одного и того же стека вызовов между задачами.
|
В начале этого поста мы говорили о вытесняющей (preemptive) и кооперативной (cooperative) многозадачности. В то время как вытесняющая многозадачность полагается на операционную систему для принудительного переключения между выполняемыми задачами, кооперативная многозадачность требует, чтобы задачи добровольно уступали контроль над CPU через операцию _yield_ на регулярной основе. Большое преимущество кооперативного подхода в том, что задачи могут сохранять своё состояние самостоятельно, что приводит к более эффективным переключениям контекста и делает возможным совместное использование одного и того же стека вызовов между задачами.
|
||||||
|
|
||||||
Это может не быть сразу очевидным, но футуры и async/await представляют собой реализацию кооперативного паттерна многозадачности:
|
Это может не быть сразу очевидным, но футуры и async/await представляют собой реализацию кооперативного паттерна многозадачности:
|
||||||
|
|
||||||
- Каждая футура, добавляемая в исполнитель, по сути является кооперативной задачей.
|
- Каждая футура, добавленная в исполнитель, по сути является кооперативной задачей.
|
||||||
- Вместо использования явной операции yield, футуры уступают контроль над ядром CPU, возвращая `Poll::Pending` (или `Poll::Ready` в конце).
|
- Вместо использования явной операции yield, футуры уступают контроль над ядром CPU, возвращая `Poll::Pending` (или `Poll::Ready` в конце).
|
||||||
- Нет ничего, что заставляло бы футуру уступать CPU. Если они захотят, они могут никогда не возвращаться из `poll`, например, бесконечно выполняя цикл.
|
- Нет ничего, что заставляло бы футуру уступать ЦПУ. Если они захотят, они могут никогда не возвращать ответ на `poll`, например, бесконечно выполняя цикл.
|
||||||
- Поскольку каждая футура может блокировать выполнение других футур в исполнителе, нам нужно доверять им, чтобы они не были вредоносными (malicious).
|
- Поскольку каждая футура может блокировать выполнение других футур в исполнителе, нам нужно доверять им, чтобы они не были вредоносными (malicious).
|
||||||
- Футуры внутренне хранят всё состояние, необходимое для продолжения выполнения при следующем вызове `poll`. При использовании async/await компилятор автоматически определяет все переменные, которые необходимы, и сохраняет их внутри сгенерированной машины состояний.
|
- Футуры хранят все состояние внутри, которое необходимо для продолжения выполнения при следующем вызове `poll`. При использовании async/await компилятор автоматически определяет все переменные, которые необходимы, и сохраняет их внутри сгенерированной машины состояний.
|
||||||
- Сохраняется только минимально необходимое состояние для продолжения.
|
- Сохраняется только минимально необходимое состояние для продолжения.
|
||||||
- Поскольку метод `poll` отдает стек вызовов при возврате, тот же стек может использоваться для опроса других футур.
|
- Поскольку метод `poll` отдает стек вызовов при возврате, тот же стек может использоваться для опроса других футур.
|
||||||
|
|
||||||
Мы видим, что футуры и async/await идеально соответствуют паттерну кооперативной многозадачности; они просто используют другую терминологию. В дальнейшем мы будем использовать термины "задача" и "футура" взаимозаменяемо.
|
Мы видим, что футуры и async/await идеально соответствуют паттерну кооперативной многозадачности; они просто используют другую терминологию. В дальнейшем мы будем использовать термины "задача" и "футура" взаимозаменяемо.
|
||||||
|
|
||||||
## Implementation
|
## Реализация
|
||||||
|
|
||||||
Теперь, когда мы понимаем, как работает кооперативная многозадачность на основе футур и 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`:
|
С достаточно свежей nightly версией мы можем начать использовать async/await в нашем `main.rs`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// in src/main.rs
|
// src/main.rs
|
||||||
|
|
||||||
async fn async_number() -> u32 {
|
async fn async_number() -> u32 {
|
||||||
42
|
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`. Для этого нам нужно создать простой тип исполнителя.
|
Чтобы запустить футуру, которую вернул `example_task`, нам нужно вызывать `poll` на ней, пока он не сигнализирует о своём завершении, возвращая `Poll::Ready`. Для этого нам нужно создать простой тип исполнителя.
|
||||||
|
|
||||||
### Task
|
### Задачи
|
||||||
|
|
||||||
Перед тем как начать реализацию исполнителя, мы создаем новый модуль `task` с типом `Task`:
|
Перед тем как начать реализацию исполнителя, мы создаем новый модуль `task` с типом `Task`:
|
||||||
|
|
||||||
@@ -815,17 +821,17 @@ pub struct Task {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Структура `Task` является обёрткой вокруг _закрепленной_, _размещённой в куче_ и _динамически диспетчеризуемой футуры_ с пустым типом `()` в качестве выходного значения. Давайте разберём её подробнее:
|
Структура `Task` является обёрткой вокруг _закрепленной_, _размещённой в куче_ и _динамически диспетчеризуемой футуры_ с пустым типом `()` в качестве выходного значения. Разберём её подробнее:
|
||||||
|
|
||||||
- Мы требуем, чтобы футура, связанная с задачей, возвращала `()`. Это означает, что задачи не возвращают никаких результатов, они просто выполняются для побочных эффектов. Например, функция `example_task`, которую мы определили выше, не имеет возвращаемого значения, но выводит что-то на экран как побочный эффект (side effect).
|
- Мы требуем, чтобы футура, связанная с задачей, возвращала `()`. Это означает, что задачи не возвращают никаких результатов, они просто выполняются для побочных эффектов. Например, функция `example_task`, которую мы определили выше, не имеет возвращаемого значения, но выводит что-то на экран как побочный эффект (side effect).
|
||||||
- Ключевое слово `dyn` указывает на то, что мы храним [_trait object_] в `Box`. Это означает, что методы на футуре диспетчеризуются динамически, позволяя хранить в типе `Task` разные типы футур. Это важно, поскольку каждая `async fn` имеет свой собственный тип, и мы хотим иметь возможность создавать несколько разных задач.
|
- Ключевое слово `dyn` указывает на то, что мы храним [_trait object_] в `Box`. Это означает, что методы на футуре диспетчеризуются динамически, позволяя хранить в типе `Task` разные типы футур. Это важно, поскольку каждая `async fn` имеет свой собственный тип, и мы хотим иметь возможность создавать несколько разных задач.
|
||||||
- Как мы узнали в [разделе о закреплении], тип `Pin<Box>` обеспечивает, что значение не может быть перемещено в памяти, помещая его в кучу и предотвращая создание `&mut` ссылок на него. Это важно, потому что фьючерсы, генерируемые async/await, могут быть самоссыльными, т.е. содержать указатели на себя, которые станут недействительными, если футура будет перемещена.
|
- Как мы узнали в [разделе о закреплении], тип `Pin<Box>` обеспечивает, что значение не может быть перемещено в памяти, помещая его в кучу и предотвращая создание `&mut` ссылок на него. Это важно, потому что футуры, генерируемые async/await, могут быть самоссылающимися, т.е. содержать указатели на себя, которые станут недействительными, если футура будет перемещена.
|
||||||
|
|
||||||
[_trait object_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html
|
[_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
|
[_dynamically dispatched_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html#trait-objects-perform-dynamic-dispatch
|
||||||
[разделе о закреплении]: #pinning
|
[разделе о закреплении]: #Закрепление
|
||||||
|
|
||||||
Чтобы разрешить создание новых структур `Task` из фьючерсов, мы создаём функцию `new`:
|
Чтобы разрешить создание новых структур `Task` из футур, мы создаём функцию `new`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// in src/task/mod.rs
|
// in src/task/mod.rs
|
||||||
@@ -859,18 +865,18 @@ impl Task {
|
|||||||
Поскольку метод [`poll`] трейта `Future` ожидает вызова на типе `Pin<&mut T>`, мы сначала используем метод [`Pin::as_mut`], чтобы преобразовать поле `self.future` типа `Pin<Box<T>>`. Затем мы вызываем `poll` на преобразованном поле `self.future` и возвращаем результат. Поскольку метод `Task::poll` должен вызываться только исполнителем, который мы создадим через мгновение, мы оставляем функцию приватной для модуля `task`.
|
Поскольку метод [`poll`] трейта `Future` ожидает вызова на типе `Pin<&mut T>`, мы сначала используем метод [`Pin::as_mut`], чтобы преобразовать поле `self.future` типа `Pin<Box<T>>`. Затем мы вызываем `poll` на преобразованном поле `self.future` и возвращаем результат. Поскольку метод `Task::poll` должен вызываться только исполнителем, который мы создадим через мгновение, мы оставляем функцию приватной для модуля `task`.
|
||||||
|
|
||||||
|
|
||||||
### Simple Executor
|
### Простой Исполнитель
|
||||||
|
|
||||||
Поскольку исполнители могут быть довольно сложными, мы намеренно начинаем с создания очень базового исполнителя, прежде чем реализовывать более продвинутого. Для этого мы сначала создаём новый подмодуль `task::simple_executor`:
|
Поскольку исполнители могут быть довольно сложными, мы намеренно начинаем с создания очень базового исполнителя, прежде чем реализовывать более продвинутого. Для этого мы сначала создаём новый подмодуль `task::simple_executor`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// in src/task/mod.rs
|
// src/task/mod.rs
|
||||||
|
|
||||||
pub mod simple_executor;
|
pub mod simple_executor;
|
||||||
```
|
```
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// in src/task/simple_executor.rs
|
// src/task/simple_executor.rs
|
||||||
|
|
||||||
use super::Task;
|
use super::Task;
|
||||||
use alloc::collections::VecDeque;
|
use alloc::collections::VecDeque;
|
||||||
@@ -895,7 +901,7 @@ impl SimpleExecutor {
|
|||||||
Структура содержит единственное поле `task_queue` типа [`VecDeque`], которое по сути является вектором, позволяющим выполнять операции добавления и удаления с обоих концов. Идея в том, что мы можем вставлять новые задачи через метод `spawn` в конец и извлекаем следующую задачу для выполнения из начала. Таким образом, мы получаем простую [FIFO очередь] ("первый пришёл — первый вышел").
|
Структура содержит единственное поле `task_queue` типа [`VecDeque`], которое по сути является вектором, позволяющим выполнять операции добавления и удаления с обоих концов. Идея в том, что мы можем вставлять новые задачи через метод `spawn` в конец и извлекаем следующую задачу для выполнения из начала. Таким образом, мы получаем простую [FIFO очередь] ("первый пришёл — первый вышел").
|
||||||
|
|
||||||
[`VecDeque`]: https://doc.rust-lang.org/stable/alloc/collections/vec_deque/struct.VecDeque.html
|
[`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
|
#### Dummy Waker
|
||||||
|
|
||||||
@@ -935,12 +941,12 @@ fn dummy_waker() -> Waker {
|
|||||||
[`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.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
|
[`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` относительно безопасной:
|
Хотя вручную создавать `RawWaker` не рекомендуется, в настоящее время нет другого способа создать заглушку `Waker`, которая ничего не делает. К счастью, тот факт, что мы хотим ничего не делать, делает реализацию функции `dummy_raw_waker` относительно безопасной:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// in src/task/simple_executor.rs
|
// src/task/simple_executor.rs
|
||||||
|
|
||||||
use core::task::RawWakerVTable;
|
use core::task::RawWakerVTable;
|
||||||
|
|
||||||
@@ -959,12 +965,12 @@ fn dummy_raw_waker() -> RawWaker {
|
|||||||
|
|
||||||
После создания `vtable` мы используем функцию [`RawWaker::new`] для создания `RawWaker`. Переданный `*const ()` не имеет значения, поскольку ни одна из функций vtable не использует его. По этой причине мы просто передаем нулевой указатель.
|
После создания `vtable` мы используем функцию [`RawWaker::new`] для создания `RawWaker`. Переданный `*const ()` не имеет значения, поскольку ни одна из функций vtable не использует его. По этой причине мы просто передаем нулевой указатель.
|
||||||
|
|
||||||
#### A `run` Method
|
#### Метод `run`
|
||||||
|
|
||||||
Теперь у нас есть способ создать экземпляр `Waker`, и мы можем использовать его для реализации метода `run` в нашем исполнителе. Самый простой метод `run` — это многократный опрос всех задач в очереди в цикле до тех пор, пока все они не будут выполнены. Это не очень эффективно, так как не использует уведомления от `Waker`, но это простой способ запустить эти штуки:
|
Теперь у нас есть способ создать экземпляр `Waker`, и мы можем использовать его для реализации метода `run` в нашем исполнителе. Самый простой метод `run` — это многократный опрос всех задач в очереди в цикле до тех пор, пока все они не будут выполнены. Это не очень эффективно, так как не использует уведомления от `Waker`, но это простой способ запустить это:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// in src/task/simple_executor.rs
|
// src/task/simple_executor.rs
|
||||||
|
|
||||||
use core::task::{Context, Poll};
|
use core::task::{Context, Poll};
|
||||||
|
|
||||||
@@ -974,7 +980,7 @@ impl SimpleExecutor {
|
|||||||
let waker = dummy_waker();
|
let waker = dummy_waker();
|
||||||
let mut context = Context::from_waker(&waker);
|
let mut context = Context::from_waker(&waker);
|
||||||
match task.poll(&mut context) {
|
match task.poll(&mut context) {
|
||||||
Poll::Ready(()) => {} // task готов
|
Poll::Ready(()) => {} // задача выполнена
|
||||||
Poll::Pending => self.task_queue.push_back(task),
|
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`, мы добавляем её в конец очереди, чтобы она была опрошена снова в следующей итерации цикла.
|
Функция использует цикл `while let`, чтобы обработать все задачи в `task_queue`. Для каждой задачи сначала создаётся тип `Context`, оборачивая экземпляр `Waker`, возвращаемый нашей функцией `dummy_waker`. Затем вызывается метод `Task::poll` с этим `context`. Если метод `poll` возвращает `Poll::Ready`, задача завершена, и мы можем продолжить с следующей задачей. Если задача всё ещё `Poll::Pending`, мы добавляем её в конец очереди, чтобы она была опрошена снова в следующей итерации цикла.
|
||||||
|
|
||||||
#### Trying It
|
#### Опробуем это
|
||||||
|
|
||||||
С нашим типом `SimpleExecutor` мы теперь можем попробовать запустить задачу, возвращаемую функцией `example_task`, в нашем `main.rs`:
|
С нашим типом `SimpleExecutor` мы теперь можем попробовать запустить задачу, возвращаемую функцией `example_task`, в нашем `main.rs`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// in src/main.rs
|
// src/main.rs
|
||||||
|
|
||||||
use blog_os::task::{Task, simple_executor::SimpleExecutor};
|
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
|
[_Interrupts_]: @/edition-2/posts/07-hardware-interrupts/index.md
|
||||||
|
|
||||||
В дальнейшем мы создадим асинхронную задачу на основе прерывания клавиатуры. Прерывание клавиатуры выбраны т.к. это хороший кандидат, т.к. это они недетерминированны, так и критично по времени задержки. Недетерминированность означает, что невозможно предсказать, когда произойдёт нажатие клавиши, поскольку это полностью зависит от пользователя. Критичность по времени задержки означает, что мы хотим обрабатывать ввод с клавиатуры своевременно, иначе пользователь почувствует задержку. Чтобы эффективно поддерживать такую задачу, исполнителю будет необходимо обеспечить надлежащую поддержку уведомлений `Waker`.
|
В дальнейшем мы создадим асинхронную задачу на основе прерывания клавиатуры. Это хороший кандидат, они недетерминированны и критичны по времени задержки. Недетерминированность означает, что невозможно предсказать, когда произойдёт нажатие клавиши, поскольку это полностью зависит от пользователя. Критичность ко времени задержки означает, что мы хотим обрабатывать ввод с клавиатуры своевременно, иначе пользователь почувствует задержку. Чтобы эффективно поддерживать такую задачу, исполнителю будет необходимо обеспечить надлежащую поддержку уведомлений `Waker`.
|
||||||
|
|
||||||
#### Scancode Queue
|
#### Очередь Скан-кодов
|
||||||
|
|
||||||
Сейчас мы обрабатываем ввод с клавиатуры непосредственно в обработчике прерываний. Это нехорошая реализация в долгосрочной перспективе, потому что обработчики прерываний должны быть как можно короче (<!-- ?TODO: время исполнения, short as possible --> ), так как они могут прерывать важную работу. Вместо этого обработчики прерываний должны выполнять только минимальный объем необходимой работы (например, считывание кода сканирования клавиатуры) и оставлять остальную работу (например, интерпретацию кода сканирования) фоновой задаче.
|
Сейчас мы обрабатываем ввод с клавиатуры непосредственно в обработчике прерываний. Это нехорошая реализация в долгосрочной перспективе, потому что обработчики прерываний должны быть как можно короче (<!-- ?TODO: время исполнения, short as possible --> ), так как они могут прерывать важную работу. Вместо этого обработчики прерываний должны выполнять только минимальный объем необходимой работы (например, считывание кода сканирования клавиатуры) и оставлять остальную работу (например, интерпретацию кода сканирования) фоновой задаче.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user