Линукс + МакОС | Окна |
---|---|
Effil — многопоточная библиотека для Lua. Это позволяет создавать собственные потоки и безопасный обмен данными. Effil был разработан, чтобы предоставить разработчикам Lua понятный и простой API.
Effil поддерживает Lua 5.1, 5.2, 5.3 и LuaJIT. Требуется соответствие компилятору C++14. Протестировано с GCC 4.9+, clang 3.8 и Visual Studio 2015.
git clone --recursive https://github.com/effil/effil effil
cd effil && mkdir build && cd build
cmake .. && make install
luarocks install effil
Как вы, возможно, знаете, существует не так много скриптовых языков с реальной поддержкой многопоточности (Lua/Python/Ruby и т. д. имеют глобальную блокировку интерпретатора, также известную как GIL). Effil решает эту проблему, запуская независимые экземпляры виртуальных машин Lua в отдельных собственных потоках и предоставляя надежные примитивы связи для создания потоков и совместного использования данных.
Библиотека Effil предоставляет три основные абстракции:
effil.thread
— предоставляет API для управления потоками.effil.table
— предоставляет API для управления таблицами. Таблицы могут быть разделены между потоками.effil.channel
— предоставляет контейнер First-First-Out для последовательного обмена данными.А также множество утилит для работы с потоками и таблицами.
local effil = require ( " effil " )
function bark ( name )
print ( name .. " barks from another thread! " )
end
-- run funtion bark in separate thread with name "Spaky"
local thr = effil . thread ( bark )( " Sparky " )
-- wait for completion
thr : wait ()
Вывод: Sparky barks from another thread!
local effil = require ( " effil " )
-- channel allow to push data in one thread and pop in other
local channel = effil . channel ()
-- writes some numbers to channel
local function producer ( channel )
for i = 1 , 5 do
print ( " push " .. i )
channel : push ( i )
end
channel : push ( nil )
end
-- read numbers from channels
local function consumer ( channel )
local i = channel : pop ()
while i do
print ( " pop " .. i )
i = channel : pop ()
end
end
-- run producer
local thr = effil . thread ( producer )( channel )
-- run consumer
consumer ( channel )
thr : wait ()
Выход:
push 1
push 2
pop 1
pop 2
push 3
push 4
push 5
pop 3
pop 4
pop 5
effil = require ( " effil " )
-- effil.table transfers data between threads
-- and behaves like regualr lua table
local storage = effil . table { string_field = " first value " }
storage . numeric_field = 100500
storage . function_field = function ( a , b ) return a + b end
storage . table_field = { fist = 1 , second = 2 }
function check_shared_table ( storage )
print ( storage . string_field )
print ( storage . numeric_field )
print ( storage . table_field . first )
print ( storage . table_field . second )
return storage . function_field ( 1 , 2 )
end
local thr = effil . thread ( check_shared_table )( storage )
local ret = thr : get ()
print ( " Thread result: " .. ret )
Выход:
first value
100500
1
2
Thread result: 3
Effil позволяет передавать данные между потоками (состояния интерпретатора Lua), используя effil.channel
, effil.table
или напрямую в качестве параметров effil.thread
.
nil
, boolean
, number
, string
lua_dump
. Повышение стоимости фиксируется в соответствии с правилами.lua_iscfunction
возвращает true) передаются просто указателем с использованием lua_tocfunction
(в исходном lua_State) и lua_pushcfunction (в новом lua_State).lua_iscfunction
возвращает true, а lua_tocfunction
возвращает nullptr. Из-за этого мы не находим способа передать его между lua_States.effil.table
. Итак, любая таблица Lua становится effil.table
. Сериализация таблиц может занять много времени для больших таблиц. Таким образом, лучше помещать данные непосредственно в effil.table
избегая сериализации таблицы. Давайте рассмотрим 2 примера: -- Example #1
t = {}
for i = 1 , 100 do
t [ i ] = i
end
shared_table = effil . table ( t )
-- Example #2
t = effil . table ()
for i = 1 , 100 do
t [ i ] = i
end
В примере №1 мы создаем обычную таблицу, заполняем ее и конвертируем в effil.table
. В этом случае Effil необходимо еще раз пройтись по всем полям таблицы. Другой способ — пример №2, где мы сначала создали effil.table
, а после этого поместили данные непосредственно в effil.table
. 2-й способ гораздо быстрее — попробуйте следовать этому принципу.
Все операции, использующие метрики времени, могут быть блокирующими или неблокирующими и использовать следующий API: (time, metric)
, где metric
— это временной интервал, например 's'
(секунды), а time
— это количество интервалов.
Пример:
thread:get()
— бесконечно ждать завершения потока.thread:get(0)
- неблокирующее получение, просто проверьте, завершен ли поток, и верните результатthread:get(50, "ms")
- блокировка ожидания в течение 50 миллисекунд.Список доступных временных интервалов:
ms
– миллисекунды;s
- секунды (по умолчанию);m
– минуты;h
- часы.Все блокирующие операции (даже в неблокирующем режиме) являются точками прерывания. Поток, зависший в такой операции, можно прервать, вызвав метод thread:cancel().
local effil = require " effil "
local worker = effil . thread ( function ()
effil . sleep ( 999 ) -- worker will hang for 999 seconds
end )()
worker : cancel ( 1 ) -- returns true, cause blocking operation was interrupted and thread was cancelled
Работая с функциями, Effil сериализует и десериализует их с помощью методов lua_dump
и lua_load
. Все значения функции сохраняются по тем же правилам, что и обычно. Если функция имеет upvalue неподдерживаемого типа, эту функцию нельзя передать в Effil. В этом случае вы получите ошибку.
Работая с функцией, Effil также может хранить среду функции ( _ENV
). Рассматривая среду как обычную таблицу, Effil будет хранить ее так же, как и любую другую таблицу. Но хранить глобальный _G
нет смысла, поэтому есть некоторые специфические:
_ENV ~= _G
). effil.thread
можно приостановить и отменить, используя соответствующие методы объекта потока thread:cancel()
и thread:pause()
.
Поток, который вы пытаетесь прервать, может быть прерван в двух точках выполнения: явной и неявной.
Явными точками являются effil.yield()
local thread = effil . thread ( function ()
while true do
effil . yield ()
end
-- will never reach this line
end )()
thread : cancel ()
Неявные точки — это вызов ловушки отладки lua, который устанавливается с помощью lua_sethook с LUA_MASKCOUNT.
Неявные точки являются необязательными и включаются только в том случае, если thread_runner.step > 0.
local thread_runner = effil . thread ( function ()
while true do
end
-- will never reach this line
end )
thread_runner . step = 10
thread = thread_runner ()
thread : cancel ()
Кроме того, поток можно отменить (но не приостановить) в любой блокирующей или неблокирующей операции ожидания.
local channel = effil . channel ()
local thread = effil . thread ( function ()
channel : pop () -- thread hangs waiting infinitely
-- will never reach this line
end )()
thread : cancel ()
Как работает отмена?
Когда вы отменяете поток, он генерирует error
Lua с сообщением "Effil: thread is cancelled"
когда он достигает любой точки прерывания. Это означает, что вы можете отловить эту ошибку, используя pcall
, но поток сгенерирует новую ошибку в следующей точке прерывания.
Если вы хотите уловить собственную ошибку, но передать ошибку отмены, вы можете использовать effil.pcall().
Статус отмененного потока будет равен cancelled
только в том случае, если он завершился с ошибкой отмены. Это означает, что если вы обнаружите ошибку отмены, поток может завершиться со статусом completed
или со статусом failed
если произойдет какая-то другая ошибка.
effil.thread
— это способ создания потока. Потоки можно останавливать, приостанавливать, возобновлять и отменять. Все операции с потоками могут быть синхронными (с необязательным таймаутом) или асинхронными. Каждый поток работает со своим собственным состоянием Lua.
Используйте effil.table
и effil.channel
для передачи данных через потоки. См. пример использования потока здесь.
runner = effil.thread(func)
Создает бегун потока. Runner порождает новый поток для каждого вызова.
ввод : func — функция Lua
вывод : runner — объект бегуна потока для настройки и запуска нового потока
Позволяет настроить и запустить новый поток.
thread = runner(...)
Запустите захваченную функцию с указанными аргументами в отдельном потоке и вернет дескриптор потока.
input : любое количество аргументов, требуемых захваченной функцией.
вывод : объект дескриптора потока.
runner.path
Это значение Lua package.path
для нового состояния. Значение по умолчанию наследует родительское состояние package.path
.
runner.cpath
Значение Lua package.cpath
для нового состояния. Значение по умолчанию наследует родительское состояние package.cpath
.
runner.step
Количество инструкций lua lua между точками отмены (где поток можно остановить или приостановить). Значение по умолчанию — 200. Если это значение равно 0, поток использует только явные точки отмены.
Дескриптор потока предоставляет API для взаимодействия с потоком.
status, err, stacktrace = thread:status()
Возвращает статус потока.
выход :
status
- строковые значения описывают статус потока. Возможные значения: "running", "paused", "cancelled", "completed" and "failed"
.err
— сообщение об ошибке, если таковое имеется. Это значение указывается только в том случае, если статус потока == "failed"
.stacktrace
— трассировка стека неудавшегося потока. Это значение указывается только в том случае, если статус потока == "failed"
.... = thread:get(time, metric)
Ожидает завершения потока и возвращает результат функции или ничего в случае ошибки.
ввод : Тайм-аут операции с точки зрения метрик времени.
вывод : результаты перехваченного вызова функции или ничего в случае ошибки.
thread:wait(time, metric)
Ожидает завершения потока и возвращает статус потока.
ввод : Тайм-аут операции с точки зрения метрик времени.
вывод : Возвращает статус потока. Вывод такой же, как thread:status()
thread:cancel(time, metric)
Прерывает выполнение потока. После вызова этой функции устанавливается флаг отмены, и поток может быть остановлен когда-нибудь в будущем (даже после завершения вызова этой функции). Чтобы быть уверенным, что поток остановлен, вызовите эту функцию с бесконечным таймаутом. Отмена завершенного потока ничего не сделает и вернет true
.
ввод : Тайм-аут операции с точки зрения метрик времени.
вывод : Возвращает true
если поток был остановлен, или false
.
thread:pause(time, metric)
Приостанавливает поток. После вызова этой функции устанавливается флаг «паузы», и поток можно приостановить когда-нибудь в будущем (даже после завершения вызова этой функции). Чтобы быть уверенным, что поток приостановлен, вызовите эту функцию с бесконечным таймаутом.
ввод : Тайм-аут операции с точки зрения метрик времени.
вывод : Возвращает true
если поток был приостановлен, или false
. Если поток завершен, функция вернет false
thread:resume()
Возобновляет приостановленную ветку. Функция немедленно возобновляет поток, если он был приостановлен. Эта функция ничего не делает для завершенного потока. Функция не имеет входных и выходных параметров.
id = effil.thread_id()
Дает уникальный идентификатор.
вывод : возвращает уникальный id
строки для текущего потока.
effil.yield()
Явная точка отмены. Функция проверяет флаги отмены или приостановки текущего потока и при необходимости выполняет соответствующие действия (отмена или приостановка потока).
effil.sleep(time, metric)
Приостановить текущий поток.
ввод : аргументы метрик времени.
effil.hardware_threads()
Возвращает количество одновременных потоков, поддерживаемых реализацией. По сути, пересылает значение из std::thread::hardware_concurrency.
вывод : количество одновременных аппаратных потоков.
status, ... = effil.pcall(func, ...)
Работает точно так же, как стандартный pcall, за исключением того, что он не обнаруживает ошибку отмены потока, вызванную вызовом thread:cancel().
вход:
выход:
true
, если ошибок не произошло, в противном случае — false
effil.table
— это способ обмена данными между потоками effil. Он ведет себя почти как стандартные таблицы Lua. Все операции с общей таблицей являются потокобезопасными. В общей таблице хранятся примитивные типы (числа, логические значения, строки), функции, таблицы, легкие пользовательские данные и пользовательские данные на основе effil. Общая таблица не хранит потоки Lua (сопрограммы) или произвольные пользовательские данные. См. примеры использования общих таблиц здесь.
Используйте общие таблицы с обычными таблицами . Если вы хотите сохранить обычную таблицу в общей таблице, effil неявно выгрузит исходную таблицу в новую общую таблицу. Общие таблицы всегда сохраняют подтаблицы как общие таблицы.
Используйте общие таблицы с функциями . Если вы сохраняете функцию в общей таблице, effil неявно выгружает эту функцию и сохраняет ее как строку (и ее значения повышаются). Все повышенные значения функции будут фиксироваться в соответствии со следующими правилами.
table = effil.table(tbl)
Создает новую пустую общую таблицу.
input : tbl
— необязательный параметр, это может быть только обычная таблица Lua, записи которой будут скопированы в общую таблицу.
вывод : новый экземпляр пустой общей таблицы. Он может быть пустым или нет, в зависимости от содержимого tbl
.
table[key] = value
Установите новый ключ таблицы с указанным значением.
вход :
key
— любое значение поддерживаемого типа. Посмотреть список поддерживаемых типовvalue
— любое значение поддерживаемого типа. Посмотреть список поддерживаемых типовvalue = table[key]
Получить значение из таблицы с указанным ключом.
input : key
— любое значение поддерживаемого типа. Посмотреть список поддерживаемых типов
вывод : value
— любое значение поддерживаемого типа. Посмотреть список поддерживаемых типов
tbl = effil.setmetatable(tbl, mtbl)
Устанавливает новую метатаблицу в общую таблицу. Аналогично стандартному setmetatable.
вход :
tbl
должна быть общей таблицей, для которой вы хотите установить метатаблицу.mtbl
должен быть обычной таблицей или общей таблицей, которая станет метатаблицей. Если это обычная таблица, effil создаст новую общую таблицу и скопирует все поля mtbl
. Установите mtbl
равным nil
, чтобы удалить метатаблицу из общей таблицы. вывод : просто возвращает tbl
с новым значением метатаблицы, аналогично стандартному методу Lua setmetatable .
mtbl = effil.getmetatable(tbl)
Возвращает текущую метатаблицу. Аналогично стандартному getmetatable
ввод : tbl
должна быть общей таблицей.
вывод : возвращает метатаблицу указанной общей таблицы. Возвращаемая таблица всегда имеет тип effil.table
. Метатаблица по умолчанию равна nil
.
tbl = effil.rawset(tbl, key, value)
Установите запись таблицы без вызова метаметода __newindex
. Аналогично стандартному сырому набору
вход :
tbl
— общая таблица.key
- ключ таблицы для переопределения. Ключ может быть любого поддерживаемого типа.value
- значение для установки. Значение может быть любого поддерживаемого типа. вывод : возвращает ту же общую таблицу tbl
value = effil.rawget(tbl, key)
Получает значение таблицы без вызова метаметода __index
. Аналогично стандартному rawget
вход :
tbl
— общая таблица.key
— ключ таблицы для получения определенного значения. Ключ может быть любого поддерживаемого типа. вывод : возвращает требуемое value
хранящееся под указанным key
effil.G
Это глобальная предопределенная общая таблица. Эта таблица всегда присутствует в любом потоке (любом состоянии Lua).
effil = require " effil "
function job ()
effil = require " effil "
effil . G . key = " value "
end
effil . thread ( job )(): wait ()
print ( effil . G . key ) -- will print "value"
result = effil.dump(obj)
Преобразует effil.table
в обычную таблицу Lua.
tbl = effil . table ({})
effil . type ( tbl ) -- 'effil.table'
effil . type ( effil . dump ( tbl )) -- 'table'
effil.channel
— это способ последовательного обмена данными между потоками effil. Это позволяет отправлять сообщение из одного потока и извлекать его из другого. Сообщение канала представляет собой набор значений поддерживаемых типов. Все операции с каналами потокобезопасны. Посмотреть примеры использования канала можно здесь.
channel = effil.channel(capacity)
Создает новый канал.
вход : дополнительная пропускная способность канала. Если capacity
равна 0
или nil
размер канала не ограничен. Емкость по умолчанию равна 0
.
вывод : возвращает новый экземпляр канала.
pushed = channel:push(...)
Отправляет сообщение в канал.
input : любое количество значений поддерживаемых типов. Несколько значений рассматриваются как одно канальное сообщение, поэтому одно нажатие на канал уменьшает пропускную способность на единицу.
вывод : pushed
равен true
, если значение(-а) соответствует пропускной способности канала, в противном случае false
.
... = channel:pop(time, metric)
Всплывающее сообщение с канала. Удаляет значения из канала и возвращает их. Если канал пуст, дождитесь появления какого-либо значения.
input : тайм-аут ожидания с точки зрения метрик времени (используется только в том случае, если канал пуст).
вывод : переменное количество значений, которые были отправлены одним вызовом Channel:push().
size = channel:size()
Получите фактическое количество сообщений в канале.
вывод : количество сообщений в канале.
Effil предоставляет собственный сборщик мусора для effil.table
и effil.channel
(и функций с захваченными значениями upvalue). Это позволяет безопасно управлять циклическими ссылками на таблицы и каналы в нескольких потоках. Однако это может привести к дополнительному использованию памяти. effil.gc
предоставляет набор методов настройки сборщика мусора effil. Но обычно вам не нужно его настраивать.
Сборщик мусора выполняет свою работу, когда effil создает новый общий объект (таблицу, канал и функции с захваченными значениями upvalue). На каждой итерации GC проверяет количество объектов. Если количество выделенных объектов становится больше, чем определенное пороговое значение, GC начинает сбор мусора. Пороговое значение рассчитывается как previous_count * step
, где previous_count
— количество объектов на предыдущей итерации (по умолчанию 100 ), а step
— числовой коэффициент, заданный пользователем (по умолчанию 2.0 ).
Например: если step
GC равен 2.0
и количество выделенных объектов равно 120
(осталось после предыдущей итерации GC), то GC начнет собирать мусор, когда количество выделенных объектов будет равно 240
.
Каждый поток представлен как отдельное состояние Lua со своим сборщиком мусора. Таким образом, объекты со временем будут удалены. Сами объекты Effil также управляются GC и используют метаметод пользовательских данных __gc
в качестве перехватчика десериализатора. Чтобы принудительно удалить объекты:
collectgarbage()
во всех потоках.effil.gc.collect()
в любом потоке.effil.gc.collect()
Принудительная сборка мусора, однако это не гарантирует удаление всех объектов effil.
count = effil.gc.count()
Показать количество выделенных общих таблиц и каналов.
вывод : возвращает текущее количество выделенных объектов. Минимальное значение — 1, effil.G
присутствует всегда.
old_value = effil.gc.step(new_value)
Получить/установить множитель шага памяти GC. По умолчанию — 2.0
. GC запускает сбор, когда количество выделенных объектов увеличивается за step
.
input : new_value
— необязательное значение шага для установки. Если оно nil
, функция просто вернет текущее значение.
вывод : old_value
— текущее (если new_value == nil
) или предыдущее (если new_value ~= nil
) значение шага.
effil.gc.pause()
Пауза ГК. Сбор мусора не будет выполняться автоматически. Функция не имеет ввода или вывода
effil.gc.resume()
Возобновить сборку мусора. Включите автоматический сбор мусора.
enabled = effil.gc.enabled()
Получите состояние GC.
вывод : возвращает true
если включен автоматический сбор мусора, или false
в противном случае. По умолчанию возвращает true
.
size = effil.size(obj)
Возвращает количество записей в объекте Effil.
ввод : obj
— общая таблица или канал.
вывод : количество записей в общей таблице или количество сообщений в канале.
type = effil.type(obj)
Потоки, каналы и таблицы — это пользовательские данные. Таким образом, type()
вернет userdata
для любого типа. Если вы хотите более точно определить тип, используйте effil.type
. Он ведет себя как обычный type()
, но может обнаруживать конкретные пользовательские данные.
ввод : obj
— объект любого типа.
вывод : строковое имя типа. Если obj
является объектом Effil, то функция возвращает строку, например effil.table
в других случаях она возвращает результат функции lua_typename.
effil . type ( effil . thread ()) == " effil.thread "
effil . type ( effil . table ()) == " effil.table "
effil . type ( effil . channel ()) == " effil.channel "
effil . type ({}) == " table "
effil . type ( 1 ) == " number "