Linux + Mac OS | Windows |
---|---|
Effil é uma biblioteca multithreading para Lua. Ele permite gerar threads nativos e troca segura de dados. Effil foi projetado para fornecer API clara e simples para desenvolvedores lua.
Effil suporta lua 5.1, 5.2, 5.3 e LuaJIT. Requer conformidade do compilador C++14. Testado com GCC 4.9+, clang 3.8 e 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 você deve saber, não existem muitas linguagens de script com suporte real a multithreading (Lua/Python/Ruby e etc possuem bloqueio de intérprete global, também conhecido como GIL). Effil resolve esse problema executando instâncias independentes de Lua VM em threads nativos separados e fornece primitivas de comunicação robustas para criação de threads e compartilhamento de dados.
A biblioteca Effil fornece três abstrações principais:
effil.thread
- fornece API para gerenciamento de threads.effil.table
- fornece API para gerenciamento de tabelas. As tabelas podem ser compartilhadas entre threads.effil.channel
- fornece contêiner First-In-First-Out para troca sequencial de dados.E vários utilitários para lidar com threads e tabelas também.
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 ()
Saída: 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 ()
Saída:
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 )
Saída:
first value
100500
1
2
Thread result: 3
Effil permite transmitir dados entre threads (estados do interpretador Lua) usando effil.channel
, effil.table
ou diretamente como parâmetros de effil.thread
.
nil
, boolean
, number
, string
lua_dump
. Os upvalues são capturados de acordo com as regras.lua_iscfunction
retorna true) são transmitidas apenas por um ponteiro usando lua_tocfunction
(no lua_State original) e lua_pushcfunction (no novo lua_State).lua_iscfunction
retorna true, mas lua_tocfunction
retorna nullptr. Por isso não encontramos forma de transmiti-lo entre lua_States.effil.table
recursivamente. Portanto, qualquer tabela Lua torna-se effil.table
. A serialização de tabelas pode levar muito tempo para tabelas grandes. Assim, é melhor colocar os dados diretamente em effil.table
evitando a serialização da tabela. Vamos considerar 2 exemplos: -- 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
No exemplo #1 criamos uma tabela regular, preenchemos e convertemos para effil.table
. Neste caso Effil precisa percorrer todos os campos da tabela mais uma vez. Outra forma é o exemplo nº 2, onde primeiro criamos effil.table
e depois colocamos os dados diretamente em effil.table
. A segunda maneira é muito mais rápida e tenta seguir esse princípio.
Todas as operações que usam métricas de tempo podem ser bloqueadas ou não e usar a seguinte API: (time, metric)
onde metric
é um intervalo de tempo como 's'
(segundos) e time
é um número de intervalos.
Exemplo:
thread:get()
- espera infinitamente pela conclusão do thread.thread:get(0)
- get sem bloqueio, basta verificar se o thread terminou e retornarthread:get(50, "ms")
- espera de bloqueio de 50 milissegundos.Lista de intervalos de tempo disponíveis:
ms
- milissegundos;s
– segundos (padrão);m
- minutos;h
- horas.Todas as operações de bloqueio (mesmo no modo sem bloqueio) são pontos de interrupção. Thread travado em tal operação pode ser interrompido invocando o 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
Trabalhando com funções Effil as serializa e desserializa usando os métodos lua_dump
e lua_load
. Todos os upvalues da função são armazenados seguindo as mesmas regras de sempre. Se a função tiver valor superior de tipo não suportado, esta função não poderá ser transmitida ao Effil. Você receberá um erro nesse caso.
Trabalhar com a função Effil também pode armazenar o ambiente da função ( _ENV
). Considerando o ambiente como uma tabela normal, Effil irá armazená-lo da mesma forma que qualquer outra tabela. Mas não faz sentido armazenar _G
global, então existem alguns específicos:
_ENV ~= _G
). O effil.thread
pode ser pausado e cancelado usando métodos correspondentes do objeto thread thread:cancel()
e thread:pause()
.
O thread que você tenta interromper pode ser interrompido em dois pontos de execução: explícito e implícito.
Os pontos explícitos são effil.yield()
local thread = effil . thread ( function ()
while true do
effil . yield ()
end
-- will never reach this line
end )()
thread : cancel ()
Os pontos implícitos são a invocação do gancho de depuração lua que é definida usando lua_sethook com LUA_MASKCOUNT.
Os pontos implícitos são opcionais e ativados somente se 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 ()
Além disso, o thread pode ser cancelado (mas não pausado) em qualquer operação de espera com ou sem bloqueio.
local channel = effil . channel ()
local thread = effil . thread ( function ()
channel : pop () -- thread hangs waiting infinitely
-- will never reach this line
end )()
thread : cancel ()
Como funciona o cancelamento?
Ao cancelar thread gera error
lua com mensagem "Effil: thread is cancelled"
ao atingir algum ponto de interrupção. Isso significa que você pode detectar esse erro usando pcall
, mas o thread irá gerar um novo erro no próximo ponto de interrupção.
Se você deseja detectar seu próprio erro, mas passar no erro de cancelamento, você pode usar effil.pcall().
O status do thread cancelado será igual a cancelled
somente se terminar com erro de cancelamento. Isso significa que se você detectar um erro de cancelamento, o thread poderá terminar com o status completed
ou com status failed
se houver algum outro erro.
effil.thread
é a maneira de criar um thread. Threads podem ser interrompidos, pausados, retomados e cancelados. Toda operação com threads pode ser síncrona (com timeout opcional) ou assíncrona. Cada thread é executado com seu próprio estado lua.
Use effil.table
e effil.channel
para transmitir dados por threads. Veja exemplo de uso de thread aqui.
runner = effil.thread(func)
Cria o executor de threads. Runner gera um novo thread para cada invocação.
entrada : func - função Lua
saída : runner - objeto thread runner para configurar e executar um novo thread
Permite configurar e executar um novo thread.
thread = runner(...)
Executa a função capturada com argumentos especificados em um thread separado e retorna o identificador do thread.
input : Qualquer número de argumentos exigidos pela função capturada.
saída : objeto de identificador de thread.
runner.path
É um valor package.path
Lua para o novo estado. O valor padrão herda o estado pai do formulário package.path
.
runner.cpath
É um valor Lua package.cpath
para o novo estado. O valor padrão herda o estado pai do formulário package.cpath
.
runner.step
Número de instruções lua lua entre pontos de cancelamento (onde o thread pode ser interrompido ou pausado). O valor padrão é 200. Se esse valor for 0, o thread usará apenas pontos de cancelamento explícitos.
O identificador de thread fornece API para interação com thread.
status, err, stacktrace = thread:status()
Retorna o status do thread.
saída :
status
- os valores da string descrevem o status do thread. Os valores possíveis são: "running", "paused", "cancelled", "completed" and "failed"
.err
- mensagem de erro, se houver. Este valor será especificado somente se thread status == "failed"
.stacktrace
- stacktrace do thread com falha. Este valor será especificado somente se thread status == "failed"
.... = thread:get(time, metric)
Aguarda a conclusão do thread e retorna o resultado da função ou nada em caso de erro.
input : Tempo limite da operação em termos de métricas de tempo
saída : Resultados da invocação da função capturada ou nada em caso de erro.
thread:wait(time, metric)
Aguarda a conclusão do thread e retorna o status do thread.
input : Tempo limite da operação em termos de métricas de tempo
saída : Retorna o status do thread. A saída é a mesma que thread:status()
thread:cancel(time, metric)
Interrompe a execução do thread. Uma vez que esta função foi invocada, o sinalizador 'cancelamento' é definido e o thread pode ser interrompido em algum momento no futuro (mesmo após a conclusão desta chamada de função). Para ter certeza de que o thread foi interrompido, invoque esta função com tempo limite infinito. O cancelamento do thread finalizado não fará nada e retornará true
.
input : Tempo limite da operação em termos de métricas de tempo
saída : Retorna true
se o thread foi interrompido ou false
.
thread:pause(time, metric)
Pausa o tópico. Uma vez que esta função foi invocada, o sinalizador 'pause' é definido e o thread pode ser pausado em algum momento no futuro (mesmo após a conclusão desta chamada de função). Para ter certeza de que o thread está pausado, invoque esta função com tempo limite infinito.
input : Tempo limite da operação em termos de métricas de tempo
saída : Retorna true
se o thread foi pausado ou false
. Se o thread for concluído, a função retornará false
thread:resume()
Retoma o tópico pausado. A função retoma o thread imediatamente se ele tiver sido pausado. Esta função não faz nada para o thread concluído. A função não possui parâmetros de entrada e saída.
id = effil.thread_id()
Fornece um identificador exclusivo.
saída : retorna id
de string exclusivo para o thread atual .
effil.yield()
Ponto de cancelamento explícito. A função verifica os sinalizadores de cancelamento ou pausa do thread atual e, se necessário, executa as ações correspondentes (cancelar ou pausar o thread).
effil.sleep(time, metric)
Suspender o tópico atual.
input : argumentos de métricas de tempo.
effil.hardware_threads()
Retorna o número de threads simultâneos suportados pela implementação. Basicamente encaminha o valor de std::thread::hardware_concurrency.
saída : número de threads de hardware simultâneos.
status, ... = effil.pcall(func, ...)
Funciona exatamente da mesma maneira que o pcall padrão, exceto que não detectará erros de cancelamento de thread causados pela chamada thread:cancel().
entrada:
saída:
true
se nenhum erro ocorreu, false
caso contrário effil.table
é uma forma de trocar dados entre threads effil. Ela se comporta quase como tabelas lua padrão. Todas as operações com tabela compartilhada são thread-safe. A tabela compartilhada armazena tipos primitivos (número, booleano, string), função, tabela, dados de usuário leves e dados de usuário baseados em efil. A tabela compartilhada não armazena threads lua (corrotinas) ou dados de usuário arbitrários. Veja exemplos de uso de tabela compartilhada aqui
Use tabelas compartilhadas com tabelas regulares . Se você deseja armazenar uma tabela regular em uma tabela compartilhada, effil irá despejar implicitamente a tabela de origem em uma nova tabela compartilhada. As tabelas compartilhadas sempre armazenam subtabelas como tabelas compartilhadas.
Use tabelas compartilhadas com funções . Se você armazenar a função na tabela compartilhada, o effil descarta implicitamente essa função e a salva como string (e seus valores positivos). Todos os valores positivos da função serão capturados de acordo com as regras a seguir.
table = effil.table(tbl)
Cria uma nova tabela compartilhada vazia .
input : tbl
- é um parâmetro opcional , pode ser apenas uma tabela Lua regular cujas entradas serão copiadas para a tabela compartilhada.
saída : nova instância de tabela compartilhada vazia. Pode estar vazio ou não, dependendo do conteúdo tbl
.
table[key] = value
Defina uma nova chave da tabela com o valor especificado.
entrada :
key
- qualquer valor do tipo suportado. Veja a lista de tipos suportadosvalue
- qualquer valor do tipo suportado. Veja a lista de tipos suportadosvalue = table[key]
Obtenha um valor da tabela com a chave especificada.
input : key
- qualquer valor do tipo suportado. Veja a lista de tipos suportados
saída : value
- qualquer valor do tipo suportado. Veja a lista de tipos suportados
tbl = effil.setmetatable(tbl, mtbl)
Define uma nova metatabela para tabela compartilhada. Semelhante ao setmetatable padrão.
entrada :
tbl
deve ser uma tabela compartilhada para a qual você deseja definir a metatabela.mtbl
deve ser uma tabela regular ou uma tabela compartilhada que se tornará uma metatabela. Se for uma tabela normal, effil criará uma nova tabela compartilhada e copiará todos os campos de mtbl
. Defina mtbl
igual a nil
para excluir a metatabela da tabela compartilhada. saída : apenas retorna tbl
com um novo valor de metatabela semelhante ao método setmetatable padrão de Lua.
mtbl = effil.getmetatable(tbl)
Retorna a metatabela atual. Semelhante ao getmetatable padrão
input : tbl
deve ser uma tabela compartilhada.
saída : retorna a metatabela da tabela compartilhada especificada. A tabela retornada sempre tem o tipo effil.table
. A metatabela padrão é nil
.
tbl = effil.rawset(tbl, key, value)
Defina a entrada da tabela sem invocar o metamétodo __newindex
. Semelhante ao rawset padrão
entrada :
tbl
é tabela compartilhada.key
- chave da tabela a ser substituída. A chave pode ser de qualquer tipo compatível.value
- valor a ser definido. O valor pode ser de qualquer tipo compatível. saída : retorna a mesma tabela compartilhada tbl
value = effil.rawget(tbl, key)
Obtém o valor da tabela sem invocar o metamétodo __index
. Semelhante ao rawget padrão
entrada :
tbl
é tabela compartilhada.key
- chave da tabela para receber um valor específico. A chave pode ser de qualquer tipo compatível. saída : retorna value
necessário armazenado em uma key
especificada
effil.G
É uma tabela compartilhada predefinida global. Esta tabela está sempre presente em qualquer thread (qualquer estado 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
em uma tabela Lua normal.
tbl = effil . table ({})
effil . type ( tbl ) -- 'effil.table'
effil . type ( effil . dump ( tbl )) -- 'table'
effil.channel
é uma forma de trocar dados sequencialmente entre threads effil. Ele permite enviar mensagens de um tópico e retirá-las de outro. A mensagem do canal é um conjunto de valores de tipos suportados. Todas as operações com canais são thread-safe. Veja exemplos de uso do canal aqui
channel = effil.channel(capacity)
Cria um novo canal.
entrada : capacidade opcional do canal. Se capacity
for igual a 0
ou nil
, o tamanho do canal é ilimitado. A capacidade padrão é 0
.
saída : retorna uma nova instância do canal.
pushed = channel:push(...)
Envia a mensagem para o canal.
input : qualquer número de valores de tipos suportados. Vários valores são considerados como uma mensagem de canal único, portanto, um push para o canal diminui a capacidade em um.
saída : pushed
é igual a true
se o valor (-s) se ajusta à capacidade do canal, false
caso contrário.
... = channel:pop(time, metric)
Mensagem pop do canal. Remove valores-s) do canal e os retorna. Se o canal estiver vazio aguarde o aparecimento de qualquer valor.
input : tempo limite de espera em termos de métricas de tempo (usado apenas se o canal estiver vazio).
saída : quantidade variável de valores que foram enviados por uma única chamada channel:push().
size = channel:size()
Obtenha a quantidade real de mensagens no canal.
saída : quantidade de mensagens no canal.
Effil fornece coletor de lixo personalizado para effil.table
e effil.channel
(e funções com upvalues capturados). Permite gerenciar com segurança referências cíclicas para tabelas e canais em múltiplos threads. No entanto, isso pode causar uso extra de memória. effil.gc
fornece um conjunto de métodos para configurar o coletor de lixo effil. Mas, geralmente você não precisa configurá-lo.
O coletor de lixo executa seu trabalho quando effil cria um novo objeto compartilhado (tabela, canal e funções com valores positivos capturados). Cada iteração GC verifica a quantidade de objetos. Se a quantidade de objetos alocados aumentar, o valor limite específico GC inicia a coleta de lixo. O valor limite é calculado como previous_count * step
, onde previous_count
é a quantidade de objetos na iteração anterior ( 100 por padrão) e step
é um coeficiente numérico especificado pelo usuário ( 2,0 por padrão).
Por exemplo: se step
do GC for 2.0
e a quantidade de objetos alocados for 120
(deixados após a iteração anterior do GC), então o GC começará a coletar lixo quando a quantidade de objetos alocados for igual a 240
.
Cada thread é representado como um estado Lua separado com seu próprio coletor de lixo. Assim, os objetos serão eventualmente excluídos. Os próprios objetos Effil também são gerenciados pelo GC e usam o metamétodo __gc
userdata como gancho desserializador. Para forçar a exclusão de objetos:
collectgarbage()
em todos os threads.effil.gc.collect()
em qualquer thread.effil.gc.collect()
Força a coleta de lixo, porém não garante a exclusão de todos os objetos efil.
count = effil.gc.count()
Mostra o número de tabelas e canais compartilhados alocados.
saída : retorna o número atual de objetos alocados. O valor mínimo é 1, effil.G
está sempre presente.
old_value = effil.gc.step(new_value)
Obtenha/defina o multiplicador de passo de memória do GC. O padrão é 2.0
. O GC aciona a coleta quando a quantidade de objetos alocados aumenta nos tempos step
.
input : new_value
é o valor opcional da etapa a ser definida. Se for nil
, a função retornará apenas um valor atual.
saída : old_value
é o valor atual (se new_value == nil
) ou anterior (se new_value ~= nil
) da etapa.
effil.gc.pause()
Pausar GC. A coleta de lixo não será realizada automaticamente. A função não possui nenhuma entrada ou saída
effil.gc.resume()
Retomar GC. Ative a coleta automática de lixo.
enabled = effil.gc.enabled()
Obtenha o estado do GC.
saída : retorne true
se a coleta automática de lixo estiver habilitada ou false
caso contrário. Por padrão retorna true
.
size = effil.size(obj)
Retorna o número de entradas no objeto Effil.
entrada : obj
é tabela ou canal compartilhado.
saída : número de entradas na tabela compartilhada ou número de mensagens no canal
type = effil.type(obj)
Threads, canais e tabelas são dados do usuário. Assim, type()
retornará userdata
para qualquer tipo. Se você deseja detectar o tipo com mais precisão, use effil.type
. Ele se comporta como type()
normal, mas pode detectar dados de usuário específicos do efil.
input : obj
é um objeto de qualquer tipo.
saída : nome da string do tipo. Se obj
for o objeto Effil, a função retornará uma string como effil.table
em outros casos, ela retornará o resultado da função 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 "