Сопрограммы (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. Ограничения и подводные камни
- Кооперативность – если внутри сопрограммы нет
yield, она «захватит» главный поток до завершения. - Стек вызовов – при
yieldвнутри C‑модуля (внешней библиотеки) может возникнуть недоступность: только функции, написанные в Lua, могут безопасноyield. coroutine.yieldможет принимать несколько значений, аresumeвозвращает их же (плюс статус).coroutine.resumeможет бросать ошибку (если внутри сопрограммы произошёлerror). Нужно обрабатыватьok == false.- Не следует хранить ссылки на завершённые сопрограммы, они остаются в памяти, пока их не собрать 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 делает две вещи:
- Создаёт сопрограмму (
coroutine.createвнутри). - Возвращает функцию, которая, при каждом вызове, автоматически делает
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 + resume | coroutine.wrap |
|---|---|---|
| Тип возвращаемого | thread (ссылка) | обычная функция |
| Как сигнализируется ошибка | resume → false, 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, который меняет только способ передачи ошибки (прокидывая её, а не возвращая). Выбирайте тот подход, который лучше соответствует вашим требованиям к обработке исключительных ситуаций. 🚀
Добавить комментарий