Tarantool обрабатывает тысячи соединений в одном потоке (TX-thread), не создавая поток ОС на каждый запрос. Достигается это файберами - легковесными "зелёными" потоками, которые планируются не ядром, а самим Tarantool. Файбер - это функция плюс собственный стек. Создавать их дёшево (карасс берётся из пула), а переключение между ними не требует системного вызова и обходится в десятки наносекунд.
Ключевое слово - кооперативная многозадачность. В отличие от вытесняющей (preemptive) модели ОС, никто не прерывает работающий файбер принудительно. Он сам отдаёт управление в момент, который называется точкой переключения (yield point). Пока файбер не сделал yield, он владеет потоком монопольно. Из этого вытекает главное свойство Tarantool: между двумя yield состояние базы консистентно, и многооператорная транзакция без yield не может быть прервана другим файбером.
Механика: event-loop, состояния, точки yield
Event-loop. В основе планировщика лежит библиотека libev. Внутри TX-потока крутится цикл событий. Есть особый системный файбер
Код: Выделить всё
schedСостояния файбера (видны через fiber.status / fiber.info):
- running - выполняется прямо сейчас (в один момент времени ровно один на поток).
- ready - создан через fiber.new, ещё ни разу не запускался; внешне через API виден как suspended.
- suspended - уступил управление (sleep, ожидание канала/условия, ожидание сети).
- dead - функция завершилась или файбер отменён.
- явные: и
Код: Выделить всё
fiber.yield()(sleep это yield плюс таймер; yield эквивалентен sleep(0));Код: Выделить всё
fiber.sleep(t) - операции ввода-вывода: сетевые вызовы, обращения к сокетам;
- коммит транзакции и обращения к диску (например ожидание 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}Создание файберов и 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)
Отмена. cancel() - асинхронный запрос. Файбер реально завершится только когда сам дойдёт до проверки: yield и большинство операций вызывают
Код: Выделить всё
fiber.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()
Про close() есть нюанс версий: исторически close выбрасывал ещё непрочитанные сообщения (после close get возвращал nil). В новом режиме (compat-опция fiber_channel_close_mode = 'new') close становится "мягким": уже лежащие сообщения можно дочитать, и только потом get вернёт nil.Главная ловушка: ёмкость по умолчанию равна 0. Канал с нулевой ёмкостью - это рандеву: put блокируется БЕСКОНЕЧНО, пока другой файбер не сделает get (слота для буферизации нет вообще). Если забыли указать capacity и ждёте поведения очереди - получите вечную блокировку.
Условные переменные (cond)
cond - примитив "разбуди меня, когда что-то изменилось". В отличие от pthread-условий, мьютекс/латч не нужен: TX однопоточный, гонок внутри потока нет.
Код: Выделить всё
local cond = fiber.cond()
cond:wait() -- усыпляет текущий файбер (неявный yield), ждёт сигнала
cond:wait(2) -- с таймаутом: true=разбудили, false=таймаут
cond:signal() -- будит ОДИН ждущий файбер, сам НЕ делает yield
cond:broadcast() -- будит ВСЕ ждущие файберы, сам НЕ делает yield
Код: Выделить всё
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)
Контрольные вопросы
- 1. Чем кооперативная многозадачность отличается от вытесняющей и какие действия являются точками yield в Tarantool?
- 2. Почему канал, созданный как fiber.channel() без аргумента, заставляет put блокироваться бесконечно?
- 3. В чём разница между cond:signal() и cond:broadcast(), и почему cond:wait() принято оборачивать в цикл с предикатом?
- 4. Что делает cancel(): когда файбер реально завершится и какой файбер нельзя отменить?