Linux + Mac OS | ventanas |
---|---|
Effil es una biblioteca multiproceso para Lua. Permite generar hilos nativos e intercambio de datos seguro. Effil ha sido diseñado para proporcionar una API clara y sencilla para los desarrolladores de lua.
Effil es compatible con lua 5.1, 5.2, 5.3 y LuaJIT. Requiere compatibilidad con el compilador C++14. Probado con GCC 4.9+, clang 3.8 y Visual Studio 2015.
git clone --recursive https://github.com/effil/effil effil
cd effil && mkdir build && cd build
cmake .. && make install
luarocks install effil
Como sabrá, no hay muchos lenguajes de secuencias de comandos con soporte real para subprocesos múltiples (Lua/Python/Ruby, etc. tienen un bloqueo de intérprete global, también conocido como GIL). Effil resuelve este problema ejecutando instancias independientes de Lua VM en subprocesos nativos separados y proporciona primitivas de comunicación sólidas para crear subprocesos y compartir datos.
La biblioteca Effil proporciona tres abstracciones principales:
effil.thread
: proporciona API para la gestión de subprocesos.effil.table
: proporciona API para la gestión de tablas. Las tablas se pueden compartir entre hilos.effil.channel
: proporciona un contenedor de primero en entrar, primero en salir para el intercambio de datos secuencial.Y también un montón de utilidades para manejar subprocesos y tablas.
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 ()
Resultado: 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 ()
Producción:
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 )
Producción:
first value
100500
1
2
Thread result: 3
Effil permite transmitir datos entre subprocesos (estados del intérprete Lua) usando effil.channel
, effil.table
o directamente como parámetros de effil.thread
.
nil
, boolean
, number
, string
lua_dump
. Los valores positivos se capturan de acuerdo con las reglas.lua_iscfunction
devuelve verdadero) se transmiten solo mediante un puntero usando lua_tocfunction
(en lua_State original) y lua_pushcfunction (en nuevo lua_State).lua_iscfunction
devuelve verdadero pero lua_tocfunction
devuelve nullptr. Debido a eso, no encontramos la manera de transmitirlo entre lua_States.effil.table
de forma recursiva. Entonces, cualquier tabla Lua se convierte en effil.table
. La serialización de tablas puede llevar mucho tiempo para una mesa grande. Por lo tanto, es mejor colocar los datos directamente en effil.table
evitando la serialización de la tabla. Consideremos 2 ejemplos: -- 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
En el ejemplo n.° 1 creamos una tabla normal, la llenamos y la convertimos a effil.table
. En este caso, Effil necesita revisar todos los campos de la tabla una vez más. Otra forma es el ejemplo n.° 2, donde primero creamos effil.table
y luego colocamos los datos directamente en effil.table
. La segunda forma es mucho más rápida: intenta seguir este principio.
Todas las operaciones que utilizan métricas de tiempo pueden ser bloqueantes o no bloqueantes y utilizan la siguiente API: (time, metric)
donde metric
es un intervalo de tiempo como 's'
(segundos) y time
es un número de intervalos.
Ejemplo:
thread:get()
- espera infinitamente a que se complete el hilo.thread:get(0)
- obtención sin bloqueo, solo verifica si el hilo finalizó y regresathread:get(50, "ms")
- espera de bloqueo durante 50 milisegundos.Lista de intervalos de tiempo disponibles:
ms
- milisegundos;s
- segundos (predeterminado);m
- minutos;h
- horas.Todas las operaciones de bloqueo (incluso en modo sin bloqueo) son puntos de interrupción. El hilo colgado en dicha operación se puede interrumpir invocando el método 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
Trabajar con funciones Effil las serializa y deserializa usando los métodos lua_dump
y lua_load
. Todos los valores superiores de las funciones se almacenan siguiendo las mismas reglas habituales. Si la función tiene un valor superior de un tipo no admitido, esta función no se puede transmitir a Effil. Obtendrá un error en ese caso.
Trabajar con la función Effil también puede almacenar el entorno de la función ( _ENV
). Considerando el entorno como una tabla normal, Effil lo almacenará de la misma manera que cualquier otra tabla. Pero no tiene sentido almacenar _G
global, por lo que hay algunos específicos:
_ENV ~= _G
). El effil.thread
se puede pausar y cancelar utilizando los métodos correspondientes del objeto de hilo thread:cancel()
y thread:pause()
.
El hilo que intentas interrumpir puede interrumpirse en dos puntos de ejecución: explícito e implícito.
Los puntos explícitos son effil.yield()
local thread = effil . thread ( function ()
while true do
effil . yield ()
end
-- will never reach this line
end )()
thread : cancel ()
Los puntos implícitos son la invocación del gancho de depuración de lua que se configura usando lua_sethook con LUA_MASKCOUNT.
Los puntos implícitos son opcionales y están habilitados solo si 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 ()
Además, el hilo se puede cancelar (pero no pausar) en cualquier operación de espera con o sin bloqueo.
local channel = effil . channel ()
local thread = effil . thread ( function ()
channel : pop () -- thread hangs waiting infinitely
-- will never reach this line
end )()
thread : cancel ()
¿Cómo funciona la cancelación?
Cuando cancela el hilo, genera error
lua con el mensaje "Effil: thread is cancelled"
cuando llega a cualquier punto de interrupción. Significa que puede detectar este error usando pcall
pero el hilo generará un nuevo error en el siguiente punto de interrupción.
Si desea detectar su propio error pero pasa el error de cancelación, puede usar effil.pcall().
El estado del hilo cancelado será igual a cancelled
solo si finalizó con un error de cancelación. Significa que si detecta un error de cancelación, el hilo puede terminar con el estado completed
o failed
si habrá algún otro error.
effil.thread
es la forma de crear un hilo. Los hilos se pueden detener, pausar, reanudar y cancelar. Todas las operaciones con subprocesos pueden ser síncronas (con tiempo de espera opcional) o asíncronas. Cada hilo se ejecuta con su propio estado lua.
Utilice effil.table
y effil.channel
para transmitir datos a través de subprocesos. Vea un ejemplo de uso de subprocesos aquí.
runner = effil.thread(func)
Crea un corredor de hilo. Runner genera un nuevo hilo para cada invocación.
entrada : func - función Lua
salida : runner - objeto de ejecución de subprocesos para configurar y ejecutar un nuevo subproceso
Permite configurar y ejecutar un nuevo hilo.
thread = runner(...)
Ejecute la función capturada con argumentos especificados en un hilo separado y devuelve el identificador del hilo.
entrada : cualquier número de argumentos requeridos por la función capturada.
salida : objeto de identificador de hilo.
runner.path
Es un valor package.path
de Lua para el nuevo estado. El valor predeterminado hereda el estado principal package.path
.
runner.cpath
Es un valor de Lua package.cpath
para el nuevo estado. El valor predeterminado hereda el estado principal del formulario package.cpath
.
runner.step
Número de instrucciones lua entre puntos de cancelación (donde el hilo se puede detener o pausar). El valor predeterminado es 200. Si este valor es 0, entonces el hilo usa solo puntos de cancelación explícitos.
El identificador de subprocesos proporciona API para la interacción con subprocesos.
status, err, stacktrace = thread:status()
Devuelve el estado del hilo.
producción :
status
: los valores de cadena describen el estado del hilo. Los valores posibles son: "running", "paused", "cancelled", "completed" and "failed"
.err
: mensaje de error, si lo hay. Este valor se especifica solo si el estado del hilo == "failed"
.stacktrace
: seguimiento de pila del subproceso fallido. Este valor se especifica solo si el estado del hilo == "failed"
.... = thread:get(time, metric)
Espera a que se complete el hilo y devuelve el resultado de la función o nada en caso de error.
entrada : tiempo de espera de operación en términos de métricas de tiempo
salida : resultados de la invocación de la función capturada o nada en caso de error.
thread:wait(time, metric)
Espera a que se complete el hilo y devuelve el estado del hilo.
entrada : tiempo de espera de operación en términos de métricas de tiempo
salida : Devuelve el estado del hilo. La salida es la misma que thread:status()
thread:cancel(time, metric)
Interrumpe la ejecución del hilo. Una vez que se invoca esta función, se establece el indicador de 'cancelación' y el hilo se puede detener en algún momento en el futuro (incluso después de realizar esta llamada a la función). Para asegurarse de que el hilo se detenga, invoque esta función con tiempo de espera infinito. La cancelación del hilo terminado no hará nada y devolverá true
.
entrada : tiempo de espera de operación en términos de métricas de tiempo
salida : Devuelve true
si el hilo se detuvo o false
.
thread:pause(time, metric)
Pausa el hilo. Una vez que se invocó esta función, se establece el indicador de 'pausa' y el hilo se puede pausar en algún momento en el futuro (incluso después de realizar esta llamada a la función). Para asegurarse de que el hilo esté en pausa, invoque esta función con tiempo de espera infinito.
entrada : tiempo de espera de operación en términos de métricas de tiempo
salida : Devuelve true
si el hilo estaba en pausa o false
. Si el hilo se completa, la función devolverá false
thread:resume()
Reanuda el hilo pausado. La función reanuda el hilo inmediatamente si se pausó. Esta función no hace nada para el hilo completado. La función no tiene parámetros de entrada ni de salida.
id = effil.thread_id()
Proporciona un identificador único.
salida : devuelve id
de cadena única para el hilo actual .
effil.yield()
Punto de cancelación explícito. La función verifica los indicadores de cancelación o pausa del hilo actual y, si es necesario, realiza las acciones correspondientes (cancelar o pausar el hilo).
effil.sleep(time, metric)
Suspender el hilo actual.
entrada : argumentos de métricas de tiempo.
effil.hardware_threads()
Devuelve el número de subprocesos simultáneos admitidos por la implementación. Básicamente reenvía el valor desde std::thread::hardware_concurrency.
salida : número de subprocesos de hardware simultáneos.
status, ... = effil.pcall(func, ...)
Funciona exactamente de la misma manera que pcall estándar, excepto que no detectará el error de cancelación de hilo causado por la llamada thread:cancel().
aporte:
producción:
true
si no ocurrió ningún error, false
en caso contrario effil.table
es una forma de intercambiar datos entre subprocesos effil. Se comporta casi como las mesas lua estándar. Todas las operaciones con tabla compartida son seguras para subprocesos. La tabla compartida almacena tipos primitivos (número, booleano, cadena), función, tabla, datos de usuario ligeros y datos de usuario basados en effil. La tabla compartida no almacena subprocesos lua (corrutinas) ni datos de usuario arbitrarios. Vea ejemplos de uso de tablas compartidas aquí
Utilice tablas compartidas con tablas normales . Si desea almacenar una tabla normal en una tabla compartida, effil volcará implícitamente la tabla de origen en una nueva tabla compartida. Las tablas compartidas siempre almacenan subtablas como tablas compartidas.
Utilice tablas compartidas con funciones . Si almacena una función en una tabla compartida, effil implícitamente descarga esta función y la guarda como una cadena (y sus valores superiores). Todos los valores ascendentes de la función se capturarán de acuerdo con las siguientes reglas.
table = effil.table(tbl)
Crea una nueva tabla compartida vacía .
entrada : tbl
: es un parámetro opcional , solo puede ser una tabla Lua normal cuyas entradas se copiarán en la tabla compartida.
salida : nueva instancia de tabla compartida vacía. Puede estar vacío o no, dependiendo del contenido tbl
.
table[key] = value
Establezca una nueva clave de tabla con el valor especificado.
aporte :
key
: cualquier valor de tipo admitido. Ver la lista de tipos admitidosvalue
: cualquier valor de tipo admitido. Ver la lista de tipos admitidosvalue = table[key]
Obtenga un valor de la tabla con la clave especificada.
entrada : key
: cualquier valor de tipo admitido. Ver la lista de tipos admitidos
salida : value
: cualquier valor del tipo admitido. Ver la lista de tipos admitidos
tbl = effil.setmetatable(tbl, mtbl)
Establece una nueva metatabla en una tabla compartida. Similar al setmetatable estándar.
aporte :
tbl
debe ser una tabla compartida para la que desea configurar la metatabla.mtbl
debe ser una tabla normal o una tabla compartida que se convertirá en una metatabla. Si es una tabla normal, effil creará una nueva tabla compartida y copiará todos los campos de mtbl
. Establezca mtbl
en nil
para eliminar la metatabla de la tabla compartida. salida : simplemente devuelve tbl
con un nuevo valor de metatabla similar al método estándar setmetatable de Lua.
mtbl = effil.getmetatable(tbl)
Devuelve la metatabla actual. Similar al getmetatable estándar
entrada : tbl
debe ser una tabla compartida.
salida : devuelve la metatabla de la tabla compartida especificada. La tabla devuelta siempre tiene el tipo effil.table
. La metatabla predeterminada es nil
.
tbl = effil.rawset(tbl, key, value)
Establecer la entrada de la tabla sin invocar el metamétodo __newindex
. Similar al rawset estándar
aporte :
tbl
es una mesa compartida.key
: clave de la tabla que se va a anular. La clave puede ser de cualquier tipo admitido.value
- valor a establecer. El valor puede ser de cualquier tipo admitido. salida : devuelve la misma tabla compartida tbl
value = effil.rawget(tbl, key)
Obtiene el valor de la tabla sin invocar el metamétodo __index
. Similar al rawget estándar
aporte :
tbl
es una mesa compartida.key
: clave de la tabla para recibir un valor específico. La clave puede ser de cualquier tipo admitido. salida : devuelve value
requerido almacenado bajo una key
especificada
effil.G
Es una tabla compartida predefinida global. Esta tabla siempre está presente en cualquier hilo (cualquier estado de 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)
Transforma effil.table
en una tabla Lua normal.
tbl = effil . table ({})
effil . type ( tbl ) -- 'effil.table'
effil . type ( effil . dump ( tbl )) -- 'table'
effil.channel
es una forma de intercambiar datos secuencialmente entre subprocesos effil. Permite enviar mensajes de un hilo y sacarlos de otro. El mensaje del canal es un conjunto de valores de tipos admitidos. Todas las operaciones con canales son seguras para subprocesos. Vea ejemplos de uso de canales aquí
channel = effil.channel(capacity)
Crea un nuevo canal.
entrada : capacidad opcional del canal. Si capacity
es igual a 0
o nil
el tamaño del canal es ilimitado. La capacidad predeterminada es 0
.
salida : devuelve una nueva instancia del canal.
pushed = channel:push(...)
Envía el mensaje al canal.
entrada : cualquier número de valores de tipos admitidos. Los valores múltiples se consideran como un mensaje de un solo canal, por lo que una pulsación en el canal disminuye la capacidad en uno.
salida : pushed
es igual a true
si el valor (-s) se ajusta a la capacidad del canal, false
en caso contrario.
... = channel:pop(time, metric)
Mensaje pop del canal. Elimina valor(es) del canal y los devuelve. Si el canal está vacío, espere a que aparezca algún valor.
entrada : tiempo de espera de espera en términos de métricas de tiempo (se usa solo si el canal está vacío).
salida : cantidad variable de valores que fueron enviados por un solo canal: llamada push().
size = channel:size()
Obtenga la cantidad real de mensajes en el canal.
salida : cantidad de mensajes en el canal.
Effil proporciona un recolector de basura personalizado para effil.table
y effil.channel
(y funciones con valores ascendentes capturados). Permite gestionar de forma segura referencias cíclicas para tablas y canales en múltiples subprocesos. Sin embargo, puede causar un uso adicional de memoria. effil.gc
proporciona un conjunto de métodos para configurar el recolector de basura effil. Pero normalmente no es necesario configurarlo.
El recolector de basura realiza su trabajo cuando effil crea un nuevo objeto compartido (tabla, canal y funciones con valores ascendentes capturados). Cada iteración de GC comprueba la cantidad de objetos. Si la cantidad de objetos asignados aumenta, entonces el valor umbral específico GC comienza a recolectar basura. El valor umbral se calcula como previous_count * step
, donde previous_count
: cantidad de objetos en la iteración anterior ( 100 de forma predeterminada) y step
es un coeficiente numérico especificado por el usuario ( 2,0 de forma predeterminada).
Por ejemplo: si step
de GC es 2.0
y la cantidad de objetos asignados es 120
(queda después de la iteración anterior de GC), entonces GC comenzará a recolectar basura cuando la cantidad de objetos asignados sea igual a 240
.
Cada hilo se representa como un estado Lua separado con su propio recolector de basura. Por lo tanto, los objetos eventualmente se eliminarán. Los objetos Effil también son administrados por GC y utilizan el metamétodo de datos de usuario __gc
como gancho deserializador. Para forzar la eliminación de objetos:
collectgarbage()
en todos los hilos.effil.gc.collect()
en cualquier hilo.effil.gc.collect()
Fuerce la recolección de basura, sin embargo, no garantiza la eliminación de todos los objetos effil.
count = effil.gc.count()
Muestra el número de tablas y canales compartidos asignados.
salida : devuelve el número actual de objetos asignados. El valor mínimo es 1, effil.G
siempre está presente.
old_value = effil.gc.step(new_value)
Obtener/configurar el multiplicador de pasos de memoria del GC. El valor predeterminado es 2.0
. GC activa la recopilación cuando la cantidad de objetos asignados crece en tiempos step
.
entrada : new_value
es el valor opcional del paso a configurar. Si es nil
, entonces la función simplemente devolverá un valor actual.
salida : old_value
es el valor actual (si new_value == nil
) o anterior (si new_value ~= nil
) del paso.
effil.gc.pause()
Pausa GC. La recogida de basura no se realizará de forma automática. La función no tiene ninguna entrada o salida.
effil.gc.resume()
Reanudar GC. Habilite la recolección automática de basura.
enabled = effil.gc.enabled()
Obtenga el estado de GC.
salida : devuelve true
si la recolección automática de basura está habilitada o false
en caso contrario. Por defecto devuelve true
.
size = effil.size(obj)
Devuelve el número de entradas en el objeto Effil.
entrada : obj
es una tabla o canal compartido.
salida : número de entradas en la tabla compartida o número de mensajes en el canal
type = effil.type(obj)
Los hilos, canales y tablas son datos de usuario. Por lo tanto, type()
devolverá userdata
para cualquier tipo. Si desea detectar el tipo con mayor precisión, utilice effil.type
. Se comporta como type()
normal, pero puede detectar datos de usuario específicos de Effil.
entrada : obj
es un objeto de cualquier tipo.
salida : nombre de cadena del tipo. Si obj
es un objeto Effil, la función devuelve una cadena como effil.table
en otros casos, devuelve el resultado de la función 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 "