Файберы, передача управления и кооперативная многозадачность
Создание файбера (fiber) — это способ Tarantool обеспечить постоянную работу логики приложения в фоновом режиме. Файбер — это набор инструкций, выполняемых с кооперативной многозадачностью: инструкции содержат сигналы передачи управления (yield), при которых управление передаётся другому файберу.
Файберы похожи на потоки выполнения в программировании. Ключевое отличие заключается в том, что потоки используют вытесняющую многозадачность, а файберы — кооперативную (см. ниже). Это даёт файберам два преимущества перед потоками:
-
Лучшая управляемость. Потоки часто зависят от планировщика потоков ядра для вытеснения занятого потока и возобновления другого, поэтому вытеснение может происходить непредсказуемо. Файберы сами передают управление для запуска другого файбера во время выполнения, поэтому передача управления контролируется логикой приложения.
-
Более высокая производительность. Потокам требуется больше ресурсов для вытеснения, так как им необходимо обращаться к ядру системы. Файберы легче и быстрее, поскольку им не нужно обращаться к ядру для передачи управления.
Однако у файберов есть некоторые ограничения по сравнению с потоками, главное из которых — отсутствие многоядерного режима. Все файберы в приложении принадлежат одному потоку, поэтому они используют то же ядро процессора, что и родительский поток. Тем не менее, для приложений Tarantool это ограничение не является существенным, так как типичное узкое место для Tarantool — это жёсткий диск, а не процессор.
Файбер обладает всеми возможностями корутины в Lua, и все концепции программирования, применимые к Lua-корутинам, применимы и к файберам. Однако в Tarantool внесены некоторые улучшения для файберов, и они используются внутри системы. Поэтому, хотя использование корутин возможно и поддерживается, рекомендуется использовать файберы.
Любой активный файбер может находиться в одном из трёх состояний:
running, suspended и ready. После завершения файбера возвращается
статус dead.
Подробнее о файберах см. в документации модуля fiber.
Передача управления (yield) — это действие в кооперативной среде, которое передаёт управление потоком от текущего файбера другому файберу, готовому к выполнению.
Любой активный файбер может находиться в одном из трёх состояний:
running, suspended и ready. После завершения файбера возвращается
статус dead. При наблюдении за файберами извне можно увидеть только
running (для текущего файбера) и suspended для любого другого
файбера, ожидающего события от цикла событий (ev) для выполнения.
После передачи управления следующий файбер в состоянии ready
извлекается из очереди и выполняется. Когда больше нет файберов в
состоянии ready, выполнение передаётся циклу событий.
После того как файбер передал управление и снова получил его, он немедленно вызывает testcancel.
Передача управления может быть явной или неявной.
Явная передача управления очевидна из вызывающего кода. Существует только два явных способа передачи управления: fiber.yield() и fiber.sleep(t).
-
fiber.yield() передаёт выполнение другому файберу в состоянии
ready, переводя себя в состояниеready, что означает, что он будет выполнен снова как можно скорее, уступая при этом место другим файберам, ожидающим выполнения. -
fiber.sleep(t) передаёт выполнение другому файберу в состоянии
readyи переводит себя в состояниеsuspendedна времяt, пока не пройдёт заданное время и цикл событий не переведёт этот файбер в состояниеready. В целом, для длительных ресурсоёмких задач рекомендуется периодически передавать управление, чтобы кооперативно уступать место другим ожидающим файберам.
С другой стороны, существует множество операций, таких как операции с сокетами, файловой системой и дисковым вводом-выводом, которые предполагают некоторое ожидание для текущего файбера, в то время как другие могут выполняться. При возникновении такой операции потенциально блокирующая операция передаётся в цикл событий, а файбер приостанавливается до тех пор, пока ресурс не будет готов для продолжения выполнения.
Список операций с неявной передачей управления:
- Установка соединения (socket).
- Чтение и запись сокета (socket).
- Операции файловой системы (из fio).
- Передача данных через каналы (fiber.channel).
- Файловый ввод-вывод (из fio).
- Операции консоли (поскольку консоль является сокетом).
- HTTP-запросы (поскольку HTTP — это операция с сокетом).
- Модификации базы данных (если они предполагают запись на диск).
- Чтение базы данных для движка vinyl.
- Вызов другого процесса (popen).
Для memtx, поскольку все данные находятся в памяти,
передача управления при запросах на чтение (таких как :select, :pairs, :get) не происходит.
Для vinyl, поскольку часть данных может отсутствовать в памяти, при чтении возможен дисковый ввод-вывод (для выборки данных с диска), а при записи — задержка в ожидании освобождения памяти.
Для memtx и vinyl, поскольку запросы на изменение данных должны записываться в WAL, обычно выполняется box.commit().
В режиме autocommit по умолчанию передача управления происходит при
следующих операциях:
- space:alter.
- space:drop.
- space:create_index.
- space:truncate.
- space:insert.
- space:replace.
- space:update.
- space:upserts.
- space:delete.
- index:update.
- index:delete.
- index:alter.
- index:drop.
- index:rename.
- box.commit (если в рамках транзакции были модификации).
Для обеспечения атомарности транзакций в транзакционном режиме к операциям модификации движка memtx применяются некоторые изменения. После выполнения box.begin или внутри вызова box.atomic ни одна операция модификации не передаёт управление — передача происходит только при box.commit или при возврате из box.atomic. При этом box.rollback не передаёт управление.
Именно поэтому выполнение отдельных команд, таких как select(),
insert(), update(), в консоли внутри транзакции без MVCC приведёт к
её прерыванию. Это связано с неявной передачей управления после
выполнения каждого фрагмента кода в консоли.
- Движок = memtx.
space:get()space:insert()
В этой последовательности происходит одна передача управления — в
конце вставки (insert), вызванной неявным коммитом;
get() не записывает ничего в WAL и
поэтому не передаёт управление.
- Движок = memtx.
box.begin()space1:get()space1:insert()space2:get()space2:insert()box.commit()
В этой последовательности происходит одна передача управления — в
конце box.commit, ни одна вставка (insert) не передаёт управление.
- Движок = vinyl.
space:get()space:insert()
В этой последовательности может быть от одной до трёх передач
управления: get() может передать управление, если данных нет в кэше,
insert() может передать управление при ожидании доступной памяти, а
также происходит неявная передача управления при коммите.
- Движок = vinyl.
box.begin()space1:get()space1:insert()space2:get()space2:insert()box.commit()
В этой последовательности команд передача управления может произойти от 1 до 5 раз.
Предположим, что в спейсе tester движка memtx есть
кортежи, в которых третье поле содержит положительную сумму в долларах.
Начнём транзакцию, снимем сумму с кортежа №1, внесём на кортеж №2 и завершим транзакцию, сделав её результаты постоянными.
tarantool> function txn_example(from, to, amount_of_money)> box.atomic(function()> box.space.tester:update(from, {{'-', 3, amount_of_money}})> box.space.tester:update(to, {{'+', 3, amount_of_money}})> end)> return "ok"> endResult:---...tarantool> txn_example({999}, {1000}, 1.00)---- "ok"...
Если wal_mode = none, то
неявной передачи управления при коммите не происходит, так как нет записей в WAL.
Если запрос выполняется через сетевой коннектор, такой как net.box, и предполагает отправку запросов на сервер и получение ответов, то это включает сетевой ввод-вывод и, следовательно, неявную передачу управления. Даже если запрос, отправляемый на сервер, не содержит неявной передачи управления. Поэтому следующая последовательность вызывает передачу управления три раза подряд — при отправке запросов в сеть и ожидании результатов.
conn.space.test:get{1}conn.space.test:get{2}conn.space.test:get{3}
Кооперативная многозадачность означает, что если активный файбер намеренно не передаёт управление, он не вытесняется другим файбером. Но активный файбер намеренно передаёт управление при достижении «точки передачи»: коммита транзакции, системного вызова или явного запроса передачи управления. Любой системный вызов, который может заблокировать выполнение, выполняется асинхронно, и любой активный файбер, ожидающий системного вызова, вытесняется, чтобы его место занял другой готовый к выполнению файбер и стал новым активным файбером.
Эта модель делает все программные блокировки ненужными: кооперативная многозадачность гарантирует отсутствие конкуренции за ресурс, состояний гонки и проблем с согласованностью памяти. Способ достижения этого прост: не использовать передачу управления, явную или неявную, в критических секциях, и никто не сможет вмешаться в выполнение кода.
Для небольших запросов, таких как простой UPDATE, INSERT, DELETE или SELECT, планирование файберов справедливо: обработка запроса, планирование записи на диск и передача управления файберу, обслуживающему следующего клиента, занимают мало времени.
Однако функция может выполнять сложные вычисления или быть написана так, что передача управления долго не происходит. Это может привести к несправедливому планированию, когда один клиент замедляет работу остальной системы, или к видимым задержкам в обработке запросов. Избегание такой ситуации — ответственность автора функции. В качестве защитного механизма можно использовать лимит времени выполнения файбера.