Сопрограмма 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:

Пример

function foo()
    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

-- файл 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 для запуска события.

Далее давайте разберём более подробный пример:

Пример

function foo (a)
    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 для решения классической задачи производителя-потребителя.

Пример

local newProductor

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
……

Разница между потоками и корутинами

Основное различие между потоками и корутинами заключается в том, что программа с несколькими потоками может выполнять несколько потоков одновременно, тогда как корутины должны работать в кооперации друг с другом.

В любой момент времени выполняется только одна корутина, и выполняющаяся корутина приостанавливается только тогда, когда её явно просят приостановиться.

Корутины в некоторой степени похожи на синхронную многопоточность. Несколько потоков, ожидающих одну и ту же блокировку потока, в некоторой степени похожи на корутины.

Основные различия кратко изложены ниже:

  • Метод планирования: Потоки обычно планируются вытесняющим образом планировщиком операционной системы, который переключает права на выполнение между разными потоками. Корутины, напротив, планируются без вытеснения; программисты явно управляют передачей прав на выполнение.

  • Конкурентность: Потоки выполняются одновременно; несколько потоков могут одновременно работать на нескольких ядрах процессора или переключать выполнение на одном ядре с помощью разделения времени. Корутины кооперативны: в каждый момент времени выполняется только одна корутина, а другие корутины должны ждать, пока текущая выполняющаяся корутина добровольно не уступит выполнение.

  • Использование памяти: Потоки обычно требуют отдельных стеков и контекстных окружений, поэтому создание и уничтожение потоков приводит к дополнительным накладным расходам. Корутины могут совместно использовать один и тот же стек и контекст, поэтому накладные расходы на создание и уничтожение корутин меньше.

  • Обмен данными: Потоки могут совместно использовать пространство памяти, но необходимо уделять внимание вопросам потокобезопасности и синхронизации. Корутины обычно обмениваются данными через передачу параметров и возвращаемые значения, и изоляция данных между разными корутинами лучше.

  • Отладка и обработка ошибок: Потоки обычно сложнее в отладке и обработке ошибок, потому что взаимодействия и параллельное выполнение нескольких потоков могут приводить к проблемам, которые трудно отлаживать. Корутины относительно проще в отладке и обработке ошибок, потому что программисты явно управляют потоком выполнения.

В целом потоки подходят для сценариев, требующих параллельного выполнения, например для использования параллелизма на многоядерных процессорах, чтобы ускорить выполнение задач. Корутины подходят для сценариев, требующих взаимодействия и координации, таких как конечные автоматы, событийно-ориентированное программирование или кооперативная обработка задач. Выбор между использованием потоков или корутин зависит от конкретных требований приложения и модели программирования.

Другие расширения