Файберы: кооперативная многозадачность, каналы

Рейтинг: 64.6% · 7 голосов
Исчерпывающий курс по Tarantool 3.x: модель данных, движки memtx и vinyl, Lua и файберы, транзакции и MVCC, SQL, конфигурация (box.cfg и декларативная 3.x), репликация и Raft, шардирование vshard, эксплуатация, безопасность. 47 уроков со схемами.
Ответить
Аватара пользователя
denis_tnt
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Файберы: кооперативная многозадачность, каналы

Сообщение denis_tnt »

Оглавление курса (47)
  1. Что такое Tarantool: in-memory СУБД и сервер приложений
  2. Архитектура изнутри: процесс, потоки, event-loop
  3. Установка и первый запуск: tt CLI, пакеты, Docker
  4. Интерактив: консоль, admin-консоль, первые команды
  5. Спейсы и кортежи: форматы, типы данных
  6. Типы индексов и их применимость
  7. Движки хранения: memtx vs vinyl
  8. DDL: схема, создание спейсов и индексов, миграции
  9. DML и выборки: insert/update/upsert, итераторы
  10. Персистентность: WAL, снапшоты, recovery
  11. Внутренности memtx: аллокаторы slab/arena, память
  12. Внутренности vinyl: LSM, компакция, тюнинг
  13. Lua и LuaJIT в Tarantool: box, модули, rocks
  14. Файберы: кооперативная многозадачность, каналы (вы здесь)
  15. Транзакции: ACID, изоляция, MVCC
  16. Хранимые процедуры, модули, организация приложения
  17. net.box: удалённые вызовы, async
  18. Пулы соединений, балансировка, реконнект
  19. Ошибки и диагностика: box.error, pcall
  20. Типы и сериализация: MsgPack, decimal, datetime, uuid
  21. SQL в Tarantool: возможности и связь с box
  22. SQL: таблицы, JOIN, подзапросы, представления
  23. SQL: подготовленные выражения, транзакции, Lua-интероп
  24. Классическая конфигурация box.cfg (legacy 1.x/2.x)
  25. Декларативная конфигурация 3.x: config.yaml, иерархия
  26. Роли и приложения в 3.x
  27. Централизованная конфигурация: etcd / config storage
  28. tt CLI глубоко: разработка, сборка, запуск
  29. Cartridge (официальный legacy) и миграция на 3.x
  30. Репликация: replicaset, топологии
  31. Механика репликации: WAL-стриминг, vclock
  32. Синхронная репликация и выборы лидера (Raft)
  33. Жизненный цикл узла: bootstrap, join, rejoin
  34. vshard: router/storage, виртуальные бакеты
  35. Решардинг и rebalancing бакетов
  36. Запросы поверх шардов: map-reduce, crud
  37. Мониторинг: метрики, Prometheus, Grafana
  38. Логирование и аудит
  39. Бэкапы и восстановление
  40. Безопасность: аутентификация, RBAC, TLS
  41. Производительность: профилирование, тюнинг
  42. Обновления: схема, rolling upgrade
  43. Деплой в продакшен: Docker, топология (официальные паттерны)
  44. Администрирование через официальный TCM (Tarantool Cluster Manager)
  45. Коннекторы: Python, Go, Java
  46. Ключевые модули (rocks): crud, metrics, queue, expirationd
  47. Capstone: шардированный отказоустойчивый кластер
Обзор: зачем нужны файберы

Tarantool обрабатывает тысячи соединений в одном потоке (TX-thread), не создавая поток ОС на каждый запрос. Достигается это файберами - легковесными "зелёными" потоками, которые планируются не ядром, а самим Tarantool. Файбер - это функция плюс собственный стек. Создавать их дёшево (карасс берётся из пула), а переключение между ними не требует системного вызова и обходится в десятки наносекунд.

Ключевое слово - кооперативная многозадачность. В отличие от вытесняющей (preemptive) модели ОС, никто не прерывает работающий файбер принудительно. Он сам отдаёт управление в момент, который называется точкой переключения (yield point). Пока файбер не сделал yield, он владеет потоком монопольно. Из этого вытекает главное свойство Tarantool: между двумя yield состояние базы консистентно, и многооператорная транзакция без yield не может быть прервана другим файбером.

Механика: event-loop, состояния, точки yield

Event-loop. В основе планировщика лежит библиотека libev. Внутри TX-потока крутится цикл событий. Есть особый системный файбер - он раздаёт управление готовым файберам и обрабатывает события libev (таймеры, готовность сокетов). Когда обычный файбер делает yield, управление возвращается планировщику, тот выбирает следующий готовый файбер и передаёт ему поток.

Состояния файбера (видны через fiber.status / fiber.info):
  • running - выполняется прямо сейчас (в один момент времени ровно один на поток).
  • ready - создан через fiber.new, ещё ни разу не запускался; внешне через API виден как suspended.
  • suspended - уступил управление (sleep, ожидание канала/условия, ожидание сети).
  • dead - функция завершилась или файбер отменён.
Что вызывает yield (точки переключения):
  • явные:

    Код: Выделить всё

    fiber.yield()
    и

    Код: Выделить всё

    fiber.sleep(t)
    (sleep это yield плюс таймер; yield эквивалентен sleep(0));
  • операции ввода-вывода: сетевые вызовы, обращения к сокетам;
  • коммит транзакции и обращения к диску (например ожидание WAL);
  • блокирующие примитивы IPC: channel:get/put в ожидании, cond:wait.
Изображение
Файберы, event-loop, yield, каналы и условия

Файбер-слайс - страховка от "жадного" файбера. Чисто вычислительный цикл без yield заблокирует весь поток. Чтобы это ловить, есть слайс - лимит времени работы без уступки управления. Он бывает warning (логируется предупреждение "fiber has not yielded for more than N seconds") и error (файбер отменяется с ошибкой FiberSliceIsExceeded). Слайс проверяется всеми операциями над спейсами и индексами, а в своём коде его можно проверять вручную через

Код: Выделить всё

fiber.check_slice()
. Настройка:

Код: Выделить всё

fiber.set_max_slice{warn=1.5, err=3}
. В Tarantool 3.x поведение по умолчанию регулируется compat-опцией fiber_slice_default.

Создание файберов и join

Код: Выделить всё

local fiber = require('fiber')

-- create: запускается немедленно, состояние running
local f1 = fiber.create(function(name)
    print('Привет, ' .. name)
end, 'Мир')

-- new: создаётся, но стартует только после yield создателя
-- joinable-файбер позволяет дождаться результата
local f2 = fiber.new(function(a, b) return a + b end, 5, 6)
f2:set_joinable(true)            -- ВАЖНО: до первого yield
local ok, result = f2:join()     -- ok=true, result=11
print(ok, result)
Правильный порядок для join: new -> set_joinable(true) -> (можно yield) -> join. Если поставить set_joinable после старта, файбер может успеть умереть. Полезные методы объекта: id(), name('worker'), status(), cancel(), storage (файбер-локальное хранилище, чистится даже у пуловых iproto-файберов после запроса).

Отмена. cancel() - асинхронный запрос. Файбер реально завершится только когда сам дойдёт до проверки: yield и большинство операций вызывают

Код: Выделить всё

fiber.testcancel()
, который кидает исключение "fiber is cancelled". Если файбер крутит цикл без yield и без testcancel, отменить его нельзя.

Каналы: обмен сообщениями между файберами

Канал - синхронная очередь фиксированной ёмкости (CSP-стиль "общайся через каналы, а не через общую память").

Код: Выделить всё

local ch = fiber.channel(10)     -- ёмкость 10 слотов

ch:put(task)        -- если полон, ждёт свободного слота
ch:put(task, 0.5)   -- ждёт не более 0.5 с, вернёт false при таймауте
local m = ch:get()  -- если пуст, ждёт сообщения
local m = ch:get(1) -- вернёт nil при таймауте 1 с или если канал закрыт
ch:is_empty(); ch:is_full(); ch:count()
ch:has_readers(); ch:has_writers()  -- кто ждёт на пустом/полном канале
ch:close()
Главная ловушка: ёмкость по умолчанию равна 0. Канал с нулевой ёмкостью - это рандеву: put блокируется БЕСКОНЕЧНО, пока другой файбер не сделает get (слота для буферизации нет вообще). Если забыли указать capacity и ждёте поведения очереди - получите вечную блокировку.
Про close() есть нюанс версий: исторически close выбрасывал ещё непрочитанные сообщения (после close get возвращал nil). В новом режиме (compat-опция fiber_channel_close_mode = 'new') close становится "мягким": уже лежащие сообщения можно дочитать, и только потом get вернёт nil.

Условные переменные (cond)

cond - примитив "разбуди меня, когда что-то изменилось". В отличие от pthread-условий, мьютекс/латч не нужен: TX однопоточный, гонок внутри потока нет.

Код: Выделить всё

local cond = fiber.cond()
cond:wait()       -- усыпляет текущий файбер (неявный yield), ждёт сигнала
cond:wait(2)      -- с таймаутом: true=разбудили, false=таймаут
cond:signal()     -- будит ОДИН ждущий файбер, сам НЕ делает yield
cond:broadcast()  -- будит ВСЕ ждущие файберы, сам НЕ делает yield
Канонический шаблон - ждать в цикле с проверкой предиката, а не "голый" wait. Иначе можно проспать сигнал (сигнал пришёл до wait) или проснуться без причины (отмена файбера тоже выводит из wait):

Код: Выделить всё

while not data_ready do
    cond:wait()
end
Частые заблуждения и грабли
  • "Файберы это параллелизм". Нет. В одном TX-потоке всё строго последовательно. Файберы дают конкурентность (ожидание I/O не блокирует других), но не используют несколько ядер для Lua-кода.
  • Тяжёлый цикл без yield. Он держит весь инстанс: запросы клиентов стоят. Лечится разбивкой работы и fiber.sleep(0)/check_slice.
  • Yield внутри транзакции. Явный или неявный yield между box.begin и box.commit может привести к ошибке (в memtx запрещён транзакционный yield без MVCC). Помните: sleep, сетевой вызов, cond:wait, channel:get - всё это yield.
  • signal() не отдаёт управление. Разбуженный файбер реально проснётся только когда сигналящий сам сделает yield. signal/broadcast лишь помечают файберы готовыми.
  • fiber.wakeup() небезопасен. Низкоуровневый wakeup на чужой файбер легко ломает его логику ожидания; для синхронизации используйте cond и каналы, а не wakeup.
  • Канал ёмкости 0 - бесконечное ожидание put (см. выше).
Мини-лаба

Запустите в консоли Tarantool. Реализуйте producer/consumer на канале и докажите кооперативность.

Код: Выделить всё

local fiber = require('fiber')
local ch = fiber.channel(3)

local producer = fiber.create(function()
    for i = 1, 5 do
        ch:put(i)
        print('put', i)
        fiber.sleep(0.1)   -- yield: даём поработать consumer
    end
    ch:close()
end)

fiber.create(function()
    while true do
        local v = ch:get()
        if v == nil then break end  -- канал закрыт
        print('got', v)
    end
    print('consumer finished')
end)
Задание: уберите fiber.sleep(0.1) у producer и объясните, в каком порядке теперь печатаются put/got и почему (подсказка: где остаются точки yield).

Контрольные вопросы
  • 1. Чем кооперативная многозадачность отличается от вытесняющей и какие действия являются точками yield в Tarantool?
  • 2. Почему канал, созданный как fiber.channel() без аргумента, заставляет put блокироваться бесконечно?
  • 3. В чём разница между cond:signal() и cond:broadcast(), и почему cond:wait() принято оборачивать в цикл с предикатом?
  • 4. Что делает cancel(): когда файбер реально завершится и какой файбер нельзя отменить?
👍3 ❤️2 🔥2 😄 🤔1
Ответить
← Предыдущая глава
Lua и LuaJIT в Tarantool: box, модули, rocks
Следующая глава →
Транзакции: ACID, изоляция, MVCC

Все главы курса «Tarantool: in-memory СУБД и сервер приложений с нуля до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «Tarantool: СУБД и сервер приложений»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость