Что такое сопрограммы в языке Lua?

Сопрограммы (coroutine) в Lua – это язык‑встроенный механизм кооперативной многозадачности.
Сопрограммы позволяют одной функции приостановить своё выполнение в любой момент, передать управление другой функции (другой сопрограмме) и позже возобновить работу точно там, где она была прервана.

В отличие от операционной системы (pre‑emptive)‑многопоточности, в Lua переключение происходит только тогда, когда кода‑выполняющий‑контекст явно вызовет yield. Поэтому сопрограммы называют «ленивыми», «кооперативными» или «зеленными» потоками.


1. Основные понятия

ПонятиеСмысл
СопрограммаОбъект, содержащий собственный стек вызовов, таблицу локальных переменных и указатель на текущую инструкцию.
coroutine.createСоздаёт новую сопрограмму (объект типа thread).
coroutine.resumeЗапускает или возобновляет выполнение сопрограммы. Возвращает true/false + результаты функции.
coroutine.yieldПриостанавливает текущую сопрограмму, передавая управление вызывающему коду (или другой сопрограмме).
coroutine.statusВозвращает состояние: "running", "suspended", "normal" или "dead".
coroutine.wrapОбёртка над create + resume, возвращающая обычную функцию, которая автоматически вызывает resume.
coroutine.runningВозвращает текущую сопрограмму и флаг, указывающий, является ли она основной (main) функцией.

2. Жизненный цикл сопрограммы

main  ──► coroutine.create(f) ──► Сопрограмма в состоянии "suspended"
      │
      └─► coroutine.resume(co) ──► Выполняет f до первого yield → "suspended"
      │
      └─► coroutine.resume(co) ──► Возобновляет после yield → …
      │
      └─► coroutine.resume(co) ──► Функция завершается → состояние "dead"
  • main – обычный (главный) поток Lua, не является сопрограммой.
  • Пока сопрограмма не yield‑нула, она «выполняется» (типа normal), но в любой момент её можно «заморозить» и передать управление обратно.

3. Простейший пример

-- Функция, которую будем выполнять в сопрограмме
local function producer()
    for i = 1, 5 do
        print("produce", i)
        coroutine.yield(i)        -- приостановка, возвращаем i вызывающему
    end
end

local co = coroutine.create(producer)

while coroutine.status(co) ~= "dead" do
    local ok, value = coroutine.resume(co)   -- запустить/возобновить
    if ok then
        print("got from producer:", value)
    else
        error(value)                         -- ошибка внутри сопрограммы
    end
end

Вывод

produce 1
got from producer: 1
produce 2
got from producer: 2
...
produce 5
got from producer: 5

4. coroutine.wrap – удобный синтаксис

local gen = coroutine.wrap(function()
    for i = 1, 3 do
        coroutine.yield(i * 10)
    end
    return "done"
end)

print(gen())   --> 10
print(gen())   --> 20
print(gen())   --> 30
print(gen())   --> "done"
print(gen())   --> error: can't resume dead coroutine

wrap возвращает обычную функцию, которая скрывает проверку статуса и возврат true/false.


5. Типичные применения

ЗадачаКак реализуется с сопрограммами
Генераторы/итераторыyield возвращает очередное значение, resume – берёт следующее.
Асинхронный ввод‑выводПисать «логически синхронный» код, где каждый блок I/O yield‑нёт, а цикл событий (например, socket.select) resume‑нет сопрограммы, когда данные готовы.
Корутино‑поиск/поиск в графеПри обходе дерева можно «сохранять» состояние обхода и переключаться между несколькими обходами без стека.
Скриптовый «поток» внутри игрыОдин «скрипт» в виде сопрограммы управляет поведением NPC, а игровой цикл просто resume‑ит его каждый тик.
Эмуляция многозадачностиСочетание нескольких небольших задач (физика, AI, UI) в одном OS‑потоке без блокирующих вызовов.

6. Ограничения и подводные камни

  1. Кооперативность – если внутри сопрограммы нет yield, она «захватит» главный поток до завершения.
  2. Стек вызовов – при yield внутри C‑модуля (внешней библиотеки) может возникнуть недоступность: только функции, написанные в Lua, могут безопасно yield.
  3. coroutine.yield может принимать несколько значений, а resume возвращает их же (плюс статус).
  4. coroutine.resume может бросать ошибку (если внутри сопрограммы произошёл error). Нужно обрабатывать ok == false.
  5. Не следует хранить ссылки на завершённые сопрограммы, они остаются в памяти, пока их не собрать GC (обычно всё ок, но «dead»‑сопрограммы не нуждаются в дальнейшем resume).

7. Полный справочный лист (Lua 5.4, почти одинаково в 5.1‑5.3)

coroutine.create (f)                 --> thread
coroutine.resume (co, ...)          --> ok, ret1, ret2, ...
coroutine.yield (...)
coroutine.status (co)                --> "running" | "suspended" | "normal" | "dead"
coroutine.isyieldable ()             --> true if current thread may yield
coroutine.wrap (f)                    --> function (...)
coroutine.running ()                 --> thread, isMain
coroutine.close (co)                 --> true or (false, err)   -- закрывает dead/unfinished

coroutine.isyieldable() полезна, если ваш код может быть вызван и из обычного потока, и из C‑сопрограммы, где yield запрещён.


8. Мини‑пример «асинхронного тайм‑аута» (без внешних библиотек)

local function sleep(seconds)
    local start = os.time()
    while os.time() - start < seconds do
        coroutine.yield()   -- просто «отдаём» управление
    end
end

local function task()
    print("начало")
    sleep(2)       -- «засыпаем» 2 секунды
    print("через 2 сек")
    sleep(1)
    print("через ещё 1 сек")
end

local co = coroutine.create(task)

while coroutine.status(co) ~= "dead" do
    coroutine.resume(co)   -- каждый tick «пробуждает» задачу
    -- в реальном движке здесь был бы кадр/таймер
end

Эта иллюстрация показывает, как с помощью yield можно разбить длительные операции на небольшие куски, позволяя другим задачам (или главному циклу) работать параллельно, хотя на уровне ОС всё всё равно в одном потоке.


9. Заключение

  • Сопрограммы – это лёгкий способ реализовать «многопоточность» внутри одного Lua‑потока, управляя точками приостановки вручную.
  • Они идеальны для генераторов, асинхронного кода и игровых скриптов, где нужен детерминированный порядок выполнения без затрат на ОС‑потоки.
  • Поскольку переключения дорогие только в виде вызова функции, их использование почти не влияет на производительность, пока не создавать миллионы активных сопрограмм одновременно.

Если хотите писать «неблокирующий» код в Lua – начните изучать API coroutine и экспериментировать с yield/resume. 🚀

В чём разница между coroutine.wrap и coroutine.create с точки зрения обработки ошибок?

Кратко

ФункцияЧто возвращаетКак «сигнализирует» об ошибке
coroutine.create(f)объект‑сопрограмма (type thread)При запуске (coroutine.resume) не бросает ошибку – возвращает false, err. Вы сами проверяете статус и, если хотите, вызываете error(err).
coroutine.wrap(f)обычная Lua‑функция‑обёрткаПри вызове этой функции бросает (error) любую ошибку, возникшую внутри сопрограммы (т. е. работает как обычный вызов функции). Если сопрограмма завершилась «мёртвой», то следующий вызов тоже бросит ошибку «cannot resume dead coroutine».

То есть разница в том, кем обработка ошибки производится – вами (resume → проверка возврата) или самим интерпретатором (wrap → автоматическое error). Ниже – разбор деталей, примеры и рекомендации.


1. coroutine.create + coroutine.resume

local co = coroutine.create(function()
    error("boom!")            -- генерируем ошибку
end)

local ok, msg = coroutine.resume(co)   -- ← одна единственная точка входа
print(ok, msg)          -- false   "boom!"
  • coroutine.resume всегда возвращает первый результат ok (логическое true/false).
  • true – сопрограмма успешно запущена или возобновлена.
  • false – внутри неё возникла ошибка (или аргументы resume некорректны).
  • Второй и последующие результаты – то, что yield/return передала из сопрограммы, или сообщение об ошибке.

Как обрабатываются ошибки

local ok, err = coroutine.resume(co)
if not ok then
    -- Мы сами решаем, что делать с ошибкой:
    -- * просто вывести
    -- * пробросить дальше
    -- * закрыть сопрограмму и т.д.
    error(err)               -- ← если хотим «прокинуть» дальше
end

Плюсы:

  • Полный контроль: можно логировать, попытаться восстановить состояние, выполнить finally‑like код, вызвать coroutine.close(co) и т.д.
  • Можно окончить сопрограмму без броска (например, когда ошибка ожидаемая).

Минусы:

  • Нужно помнить проверять ok после каждого resume.
  • При вложенных вызовах (корутинки вызывают друг друга) каждый уровень обязан проверять возврат.

2. coroutine.wrap

local f = coroutine.wrap(function()
    error("boom!")
end)

f()                -- → бросает ошибку "boom!"

coroutine.wrap делает две вещи:

  1. Создаёт сопрограмму (coroutine.create внутри).
  2. Возвращает функцию, которая, при каждом вызове, автоматически делает coroutine.resume и прокидывает любые ошибки как обычный error.

Что именно происходит внутри

function coroutine.wrap (f)
    local co = coroutine.create(f)
    return function (...)
        local ok, res = coroutine.resume(co, ...)
        if ok then
            return res               -- (и остальные возвращаемые значения)
        else
            error(res, 2)            -- бросаем, поднимая стек на уровень выше
        end
    end
end
  • error(res, 2) поднимает ошибку через обёртку, так что вы никогда не получаете false, err от вызова полученной функции — либо получаете корректный результат, либо получаете обычный error.

Специфическое поведение при «мертвой» сопрограмме

local g = coroutine.wrap(function()
    coroutine.yield(1)
end)

print(g())   --> 1          -- первый вызов, всё ок
print(g())   --> error: cannot resume dead coroutine

После того как сопрограмма завершилась (все её коды выполнены), следующий вызов функции‑обёртки бросит стандартную ошибку cannot resume dead coroutine. Это удобно, потому что вы сразу замечаете неправильный порядок вызовов, но если вам нужен более «мягкий» способ определить конец (например, просто nil), то create/resume предпочтительнее.

Плюсы wrap

ПлюсПочему
Прозрачность – вызываете функцию так же, как обычную, без проверки статуса.Меньше бойлерплейта в коде.
Автоматическое пробрасывание ошибок → ваша программа ведёт себя, как если бы ошибка произошла в обычной функции.Удобно в синхронных API (генераторы, итераторы).
Можно использовать в for … in конструкциях (coroutine.wrap часто используется в пользовательских итераторах).Позволяет писать «чистый» Lua‑код.
МинусПочему
Нет доступа к ok‑флагу, т. е. вы не можете решить «просто логировать, а не ломать процесс».Требует pcall/xpcall, если хочется перехватить ошибку.
Не видно, когда сопрограмма «умерла», без генерации исключения.Иногда нужно отличать «нормальный конец» от «ошибка».
Не совместимо с кодом, где требуется явно вызвать coroutine.close (только у thread‑объекта).Если вам нужен coroutine.close, используйте create.

3. Примеры сравнения

3.1. Обычный «генератор» (wrap)

local function range(n)
    return coroutine.wrap(function()
        for i = 1, n do
            coroutine.yield(i)
        end
    end)
end

for v in range(3) do
    print(v)           -- 1 2 3  ( без ошибок )
end

3.2. Генератор с контролируемой ошибкой (create)

local function safe_range(n)
    local co = coroutine.create(function()
        for i = 1, n do
            if i == 2 then error("bad number") end
            coroutine.yield(i)
        end
    end)
    return function()
        local ok, v = coroutine.resume(co)
        if not ok then
            return nil, v        -- возвращаем ошибку как второе значение
        end
        return v
    end
end

local gen = safe_range(3)
while true do
    local v, err = gen()
    if err then
        print("error:", err)    -- Поймали и обработали, не падая
        break
    end
    if v == nil then break end
    print(v)                    -- печатает 1, потом бросит ошибку
end

В wrap‑версии ошибка «пробрасывается», в create‑версии мы возвращаем её как обычный результат – это даёт гибкость, если ошибка не должна останавливать программу.


4. Как правильно выбирать

СценарийРекомендация
Простая итерация / генератор, где ошибка считается «фатальной» (пример: чтение файла, а не найдено → бросить).coroutine.wrap.
Асинхронный цикл, где нужно обработать/логировать ошибку, но продолжить работу (например, сервер обрабатывает несколько клиентских задач).coroutine.create + coroutine.resume + ручная проверка ok.
Библиотека, которую используют разные пользователи; вы хотите дать им максимум контроля.coroutine.create.
Краткие скрипты/игровые скрипты, где читаемость важнее тонкой обработки.coroutine.wrap.
Необходимо явно закрывать сопрограмму (например, использовать coroutine.close в Lua 5.4).coroutine.create.

5. Тонкости в разных версиях Lua

Версияcoroutine.wrap
Lua 5.1При ошибке в сопрограмме не бросает её сразу, а возвращает её как второй результат (false, err). Это поведение отличается от 5.2+. Поэтому в 5.1 wrap не полностью заменяет resume.
Lua 5.2 и выше (включая 5.3, 5.4)wrap бросает ошибку (error). Это делает его более удобным, но изменило семантику по сравнению с 5.1.
LuaJITСохраняет поведение Lua 5.1 (т.к. основан на ней). Поэтому в LuaJIT wrap тоже возвращает false, err. При написании кода, рассчитанного на обе среды, следует не полагаться на автобросок.

Вывод: если ваш код должен работать и в Lua 5.1 / LuaJIT, а также в более новых версиях, проверяйте type(res) == "boolean" после вызова функции‑обёртки, либо просто используйте coroutine.create / resume.


6. Как «перехватывать» ошибки из wrap

Если всё‑равно нужна возможность отловить ошибку, используйте обычный pcall/xpcall:

local gen = coroutine.wrap(function()
    error("oops")
end)

local ok, result = pcall(gen)   -- ok == false, result – сообщение
print(ok, result)              -- false   "oops"

Это дает гибридный подход: wrap сохраняет лаконичность, а pcall позволяет обработать ошибку, не бросая её в глобальный уровень.


7. Итого (ключевые пункты)

Аспектcoroutine.create + resumecoroutine.wrap
Тип возвращаемогоthread (ссылка)обычная функция
Как сигнализируется ошибкаresumefalse, err (нужно проверять)Вызов функции → error(err) (автоматический бросок)
Контроль над окончаниемВы можете самостоятельно проверять coroutine.status и вызывать coroutine.closeПри завершении – следующий вызов бросает «cannot resume dead coroutine»
СовместимостьРаботает одинаково во всех версиях (5.1‑5.4)5.2+ – бросок; 5.1/ЛуаJIT – возвращает false, err
Когда использоватьТребуется тонкая обработка, логирование, закрытие, работа в 5.1/LuaJITТребуется короткая, «как обычная функция» API, ошибки считаются фатальными, код читается легче

Помните: wrap – это «синтаксический сахар» над create/resume, который меняет только способ передачи ошибки (прокидывая её, а не возвращая). Выбирайте тот подход, который лучше соответствует вашим требованиям к обработке исключительных ситуаций. 🚀

Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *