Сопрограмма Lua
Что такое сопрограмма?
Сопрограммы Lua очень похожи на потоки: у них есть независимые стеки, независимые локальные переменные и независимые указатели инструкций, при этом они также совместно используют глобальные переменные и большинство других вещей с другими сопрограммами.
Сопрограмму можно понимать как особый вид потока, который может приостанавливать и возобновлять своё выполнение, тем самым позволяя реализовать невытесняющую многозадачность.
Сопрограммы — очень мощная возможность, но их также сложно использовать.
Базовый синтаксис
Корутины поддерживаются модулемcoroutine.
При использовании корутин можно использоватьcoroutine.create, чтобы создать новый объект корутины, и использоватьcoroutine.resume, чтобы запустить её выполнение. Корутина можетcoroutine.yieldдобровольно приостановить своё выполнение и вернуть управление вызывающему.
| Метод | Описание |
|---|---|
| coroutine.create() | Создаёт корутину, возвращает корутину, параметр — функция. При использовании с resume она пробуждает вызов функции. |
| coroutine.resume() | Перезапускает корутину, используется вместе с create. |
| coroutine.yield() | Приостанавливает корутину, переводит корутину в состояние suspended. При использовании с resume это может дать множество полезных эффектов. |
| coroutine.status() | Проверяет статус корутины. Примечание: корутина имеет три состояния: dead, suspended, running. Смотрите программу ниже, чтобы узнать, когда возникают эти состояния. |
| coroutine.wrap() | Создаёт корутину, возвращает функцию. После вызова этой функции вы входите в корутину. Это дублирует функциональность create. |
| coroutine.running() | Возвращает выполняющуюся корутину. Корутина — это поток. При использовании running возвращается идентификатор потока корутины. |
Следующий пример демонстрирует, как использовать корутины Lua:
Пример
print("Корутина foo начала выполнение")
local value = coroutine.yield("Приостановка выполнения foo")
print("Корутина foo возобновила выполнение, переданное значение: " .. tostring(value))
print("Корутина foo завершила выполнение")
end
-- Создать корутину
local co = coroutine.create(foo)
-- Запустить корутину
local status, result = coroutine.resume(co)
print(result) -- Вывод: Приостановка выполнения foo
-- Возобновить выполнение сопрограммы и передать значение
status, result = coroutine.resume(co, 42)
print(result) -- Вывод: Сопрограмма foo возобновила выполнение, переданное значение: 42
В приведённом выше примере мы определили функцию с именем foo как сопрограмму. Внутри функции мы использовали coroutine.yield, чтобы приостановить выполнение сопрограммы и вернуть значение.
В основной программе мы использовали coroutine.create для создания объекта сопрограммы и coroutine.resume для запуска её выполнения.
После первого вызова coroutine.resume сопрограмма приостанавливается на coroutine.yield и возвращает значение в основную программу. Затем мы снова вызываем coroutine.resume, передавая значение в качестве параметра для возобновления выполнения сопрограммы.
Выполнение приведенного выше кода дает следующий вывод:
协同程序 foo 开始执行 暂停 foo 的执行 协同程序 foo 恢复执行,传入的值为: 42 协同程序 foo 结束执行 nil
Следует отметить, что состояние сопрограммы можно получить с помощью функции coroutine.status. Проверяя состояние, можно определить статус выполнения сопрограммы (например, running, suspended, ended и т. д.).
В следующем примере демонстрируется использование вышеуказанных методов:
файл coroutine_test.lua
-- Создать новый объект сопрограммы co, где функция сопрограммы выводит переданный параметр i
co = coroutine.create(
функция(i)
print(i);
end
)
-- Используйте coroutine.resume для запуска выполнения корутины co, передав параметр 1. Корутина начинает выполняться и выводит результат 1.
coroutine.resume(co, 1) -- 1
-- Проверьте состояние корутины co с помощью coroutine.status; вывод — dead, что указывает на завершение выполнения корутины.
print(coroutine.status(co)) -- dead
print("----------")
-- Используйте coroutine.wrap для создания обёртки корутины, преобразующей функцию корутины в напрямую вызываемый объект функции.
co = coroutine.wrap(
функция(i)
print(i);
end
)
co(1)
print("----------")
-- Создайте другой объект корутины co2, в котором функция корутины выводит числа от 1 до 10 в цикле, и когда цикл достигает 3, она выводит текущее состояние корутины и выполняющийся поток.
co2 = coroutine.create(
функция()
for i=1,10 do
print(i)
if i == 3 then
print(coroutine.status(co2)) --running
print(coroutine.running()) --thread:XXXXXX
end
coroutine.yield()
end
end
)
-- Вызовите coroutine.resume несколько раз, чтобы запустить выполнение корутины co2.
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3
-- Проверьте состояние корутины co2 через coroutine.status, вывод — suspended, что указывает на то, что корутина приостановлена.
print(coroutine.status(co2)) -- suspended
print(coroutine.running())
print("----------")
Приведённый выше пример выдаёт следующий вывод:
1 dead ---------- 1 ---------- 1 2 3 running thread: 0x7fb801c05868 false suspended thread: 0x7fb801c04c88 true ----------
Как видно из coroutine.running, на нижнем уровне корутина реализована как поток.
Когда вы создаёте корутину, она регистрирует событие в новом потоке.
Когда resume используется для запуска события, выполняется функция корутины, созданная с помощью create. Когда встречается yield, это означает приостановку текущего потока и ожидание ещё одного resume для запуска события.
Далее давайте разберём более подробный пример:
Пример
print("вывод функции foo", a)
return coroutine.yield(2 * a) -- Возвращает значение 2*a
end
co = coroutine.create(function (a , b)
print("Вывод при первом выполнении корутины", a, b) -- тело корутины 1 10
local r = foo(a + 1)
print("Вывод при втором выполнении корутины", r)
local r, s = coroutine.yield(a + b, a - b) -- Значения a и b передаются при первом вызове корутины
print("Вывод при третьем выполнении корутины", r, s)
return b, "Конец корутины" -- Значение b передается при втором вызове корутины
end)
print("главная", coroutine.resume(co, 1, 10)) -- true, 4
print("--Разделитель----")
print("главная", coroutine.resume(co, "r")) -- true 11 -9
print("---Separator---")
print("main", coroutine.resume(co, "x", "y")) -- true 10 end
print("---Separator---")
print("main", coroutine.resume(co, "x", "y")) -- cannot resume dead coroutine
print("---Separator---")
Приведённый выше пример выдаёт следующий вывод:
第一次协同程序执行输出 1 10 foo 函数输出 2 main true 4 --分割线---- 第二次协同程序执行输出 r main true 11 -9 ---分割线--- 第三次协同程序执行输出 x y main true 10 结束协同程序 ---分割线--- main false cannot resume dead coroutine ---分割线---
Приведённый выше пример выполняется следующим образом:
- Вызовите resume, чтобы разбудить сопрограмму. Операция resume возвращает true при успехе, иначе возвращает false.
- Сопрограмма выполняется.
- Выполняется до оператора yield.
- yield приостанавливает сопрограмму, и первый resume возвращает управление. (Примечание: здесь yield возвращает управление, а параметрами являются аргументы resume).
- Второй resume снова будит сопрограмму. (Примечание: здесь среди аргументов resume все, кроме первого, будут служить параметрами yield).
- yield возвращает управление.
- Сопрограмма продолжает выполняться.
- Если после завершения работы сопрограммы вы продолжите вызывать метод resume, он выведет: cannot resume dead coroutine.
Сила объединения resume и yield заключается в том, что resume находится в основной программе и передаёт внешнее состояние (данные) в корутину, а yield возвращает внутреннее состояние (данные) обратно в основную программу.
Задача производителя-потребителя
Теперь я буду использовать корутины Lua для решения классической задачи производителя-потребителя.
Пример
function productor()
local i = 0
while true do
i = i + 1
send(i) -- Отправить произведённый элемент потребителю
end
end
function consumer()
while true do
local i = receive() -- Получить элемент от производителя
print(i)
end
end
function receive()
local status, value = coroutine.resume(newProductor)
return value
end
function send(x)
coroutine.yield(x) -- x представляет значение, которое нужно отправить. После возврата значения приостановить корутину.
end
-- Запуск программы
newProductor = coroutine.create(productor)
consumer()
Приведённый выше пример выдаёт следующий вывод:
1 2 3 4 5 6 7 8 9 10 11 12 13 ……
Разница между потоками и корутинами
Основное различие между потоками и корутинами заключается в том, что программа с несколькими потоками может выполнять несколько потоков одновременно, тогда как корутины должны работать в кооперации друг с другом.
В любой момент времени выполняется только одна корутина, и выполняющаяся корутина приостанавливается только тогда, когда её явно просят приостановиться.
Корутины в некоторой степени похожи на синхронную многопоточность. Несколько потоков, ожидающих одну и ту же блокировку потока, в некоторой степени похожи на корутины.
Основные различия кратко изложены ниже:
Метод планирования: Потоки обычно планируются вытесняющим образом планировщиком операционной системы, который переключает права на выполнение между разными потоками. Корутины, напротив, планируются без вытеснения; программисты явно управляют передачей прав на выполнение.
Конкурентность: Потоки выполняются одновременно; несколько потоков могут одновременно работать на нескольких ядрах процессора или переключать выполнение на одном ядре с помощью разделения времени. Корутины кооперативны: в каждый момент времени выполняется только одна корутина, а другие корутины должны ждать, пока текущая выполняющаяся корутина добровольно не уступит выполнение.
Использование памяти: Потоки обычно требуют отдельных стеков и контекстных окружений, поэтому создание и уничтожение потоков приводит к дополнительным накладным расходам. Корутины могут совместно использовать один и тот же стек и контекст, поэтому накладные расходы на создание и уничтожение корутин меньше.
Обмен данными: Потоки могут совместно использовать пространство памяти, но необходимо уделять внимание вопросам потокобезопасности и синхронизации. Корутины обычно обмениваются данными через передачу параметров и возвращаемые значения, и изоляция данных между разными корутинами лучше.
Отладка и обработка ошибок: Потоки обычно сложнее в отладке и обработке ошибок, потому что взаимодействия и параллельное выполнение нескольких потоков могут приводить к проблемам, которые трудно отлаживать. Корутины относительно проще в отладке и обработке ошибок, потому что программисты явно управляют потоком выполнения.
В целом потоки подходят для сценариев, требующих параллельного выполнения, например для использования параллелизма на многоядерных процессорах, чтобы ускорить выполнение задач. Корутины подходят для сценариев, требующих взаимодействия и координации, таких как конечные автоматы, событийно-ориентированное программирование или кооперативная обработка задач. Выбор между использованием потоков или корутин зависит от конкретных требований приложения и модели программирования.
Другие расширения