Linux + MacOS | Fenêtres |
---|---|
Effil est une bibliothèque multithread pour Lua. Il permet de générer des threads natifs et un échange de données sécurisé. Effil a été conçu pour fournir une API claire et simple aux développeurs Lua.
Effil prend en charge Lua 5.1, 5.2, 5.3 et LuaJIT. Nécessite la conformité du compilateur C++14. Testé avec GCC 4.9+, clang 3.8 et Visual Studio 2015.
git clone --recursive https://github.com/effil/effil effil
cd effil && mkdir build && cd build
cmake .. && make install
luarocks install effil
Comme vous le savez peut-être, il n'existe pas beaucoup de langages de script avec un véritable support multithreading (Lua/Python/Ruby, etc. ont un verrouillage global de l'interpréteur, alias GIL). Effil résout ce problème en exécutant des instances indépendantes de VM Lua dans des threads natifs distincts et fournit des primitives de communication robustes pour la création de threads et le partage de données.
La bibliothèque Effil fournit trois abstractions majeures :
effil.thread
- fournit une API pour la gestion des threads.effil.table
- fournit une API pour la gestion des tables. Les tables peuvent être partagées entre les threads.effil.channel
- fournit un conteneur First-In-First-Out pour l'échange de données séquentiel.Et de nombreux utilitaires pour gérer également les threads et les tables.
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 ()
Résultat : 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 ()
Sortir:
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 )
Sortir:
first value
100500
1
2
Thread result: 3
Effil permet de transmettre des données entre les threads (états de l'interpréteur Lua) en utilisant effil.channel
, effil.table
ou directement en tant que paramètres de effil.thread
.
nil
, boolean
, number
, string
lua_dump
. Les hausses de valeurs sont capturées conformément aux règles.lua_iscfunction
renvoie true) sont transmises simplement par un pointeur en utilisant lua_tocfunction
(dans lua_State d'origine) et lua_pushcfunction (dans le nouveau lua_State).lua_iscfunction
renvoie true mais lua_tocfunction
renvoie nullptr. Pour cette raison, nous ne trouvons pas de moyen de le transmettre entre lua_States.effil.table
de manière récursive. Ainsi, n'importe quelle table Lua devient effil.table
. La sérialisation des tables peut prendre beaucoup de temps pour les grandes tables. Ainsi, il est préférable de mettre les données directement dans effil.table
en évitant une sérialisation de table. Considérons 2 exemples : -- 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
Dans l'exemple n°1, nous créons une table normale, la remplissons et la convertissons en effil.table
. Dans ce cas, Effil doit parcourir à nouveau tous les champs de la table. Une autre façon est l'exemple n°2 où nous avons d'abord créé effil.table
et ensuite nous avons mis les données directement dans effil.table
. La 2ème méthode est beaucoup plus rapide, essayez de suivre ce principe.
Toutes les opérations qui utilisent des métriques de temps peuvent être bloquantes ou non et utiliser l'API suivante : (time, metric)
où metric
est un intervalle de temps comme 's'
(secondes) et time
est un nombre d'intervalles.
Exemple:
thread:get()
- attend indéfiniment la fin du thread.thread:get(0)
- get non bloquant, il suffit de vérifier si le thread est terminé et de revenirthread:get(50, "ms")
- attente de blocage de 50 millisecondes.Liste des intervalles de temps disponibles :
ms
- millisecondes ;s
- secondes (par défaut) ;m
- minutes ;h
- heures.Toutes les opérations bloquantes (même en mode non bloquant) sont des points d'interruption. Le thread suspendu lors d’une telle opération peut être interrompu en appelant la méthode 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
Travailler avec des fonctions Effil les sérialise et les désérialise à l'aide des méthodes lua_dump
et lua_load
. Toutes les valeurs positives de la fonction sont stockées selon les mêmes règles que d'habitude. Si la fonction a une valeur positive d'un type non pris en charge, cette fonction ne peut pas être transmise à Effil. Vous obtiendrez une erreur dans ce cas.
Travailler avec la fonction Effil peut également stocker l'environnement de fonction ( _ENV
). Considérant l'environnement comme une table ordinaire, Effil le stockera de la même manière que n'importe quelle autre table. Mais cela n'a pas de sens de stocker global _G
, il y en a donc quelques-uns spécifiques :
_ENV ~= _G
). Le effil.thread
peut être mis en pause et annulé à l'aide des méthodes correspondantes de l'objet thread thread:cancel()
et thread:pause()
.
Le thread que vous essayez d'interrompre peut être interrompu en deux points d'exécution : explicite et implicite.
Les points explicites sont effil.yield()
local thread = effil . thread ( function ()
while true do
effil . yield ()
end
-- will never reach this line
end )()
thread : cancel ()
Les points implicites sont l'invocation du hook de débogage Lua qui est définie à l'aide de lua_sethook avec LUA_MASKCOUNT.
Les points implicites sont facultatifs et activés uniquement 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 ()
De plus, le thread peut être annulé (mais pas mis en pause) dans toute opération d'attente bloquante ou non bloquante.
local channel = effil . channel ()
local thread = effil . thread ( function ()
channel : pop () -- thread hangs waiting infinitely
-- will never reach this line
end )()
thread : cancel ()
Comment fonctionne l'annulation ?
Lorsque vous annulez le fil, il génère error
Lua avec le message "Effil: thread is cancelled"
lorsqu'il atteint un point d'interruption. Cela signifie que vous pouvez détecter cette erreur en utilisant pcall
mais que le thread générera une nouvelle erreur au prochain point d'interruption.
Si vous souhaitez détecter votre propre erreur mais transmettre l'erreur d'annulation, vous pouvez utiliser effil.pcall().
Le statut du fil de discussion annulé sera égal à cancelled
uniquement s'il s'est terminé avec une erreur d'annulation. Cela signifie que si vous détectez une erreur d'annulation, le thread peut se terminer avec un statut completed
ou un statut failed
s'il y a une autre erreur.
effil.thread
est le moyen de créer un fil de discussion. Les discussions peuvent être arrêtées, mises en pause, reprises et annulées. Toutes les opérations avec les threads peuvent être synchrones (avec délai d'attente facultatif) ou asynchrones. Chaque thread s'exécute avec son propre état Lua.
Utilisez effil.table
et effil.channel
pour transmettre des données via des threads. Voir un exemple d'utilisation du thread ici.
runner = effil.thread(func)
Crée un exécuteur de thread. Runner génère un nouveau thread pour chaque invocation.
entrée : func - Fonction Lua
sortie : runner - objet thread runner pour configurer et exécuter un nouveau thread
Permet de configurer et d'exécuter un nouveau thread.
thread = runner(...)
Exécutez la fonction capturée avec les arguments spécifiés dans un thread séparé et renvoie le handle du thread.
input : n'importe quel nombre d'arguments requis par la fonction capturée.
sortie : objet handle de thread.
runner.path
Est une valeur Lua package.path
pour le nouvel état. La valeur par défaut hérite de l'état parent du formulaire package.path
.
runner.cpath
Est une valeur Lua package.cpath
pour le nouvel état. La valeur par défaut hérite de l'état parent du formulaire package.cpath
.
runner.step
Nombre d'instructions Lua entre les points d'annulation (où le thread peut être arrêté ou mis en pause). La valeur par défaut est 200. Si cette valeur est 0, le thread utilise uniquement des points d'annulation explicites.
Le handle de thread fournit une API pour l’interaction avec le thread.
status, err, stacktrace = thread:status()
Renvoie l’état du fil.
sortir :
status
- les valeurs de chaîne décrivent l'état du thread. Les valeurs possibles sont : "running", "paused", "cancelled", "completed" and "failed"
.err
- message d'erreur, le cas échéant. Cette valeur est spécifiée uniquement si thread status == "failed"
.stacktrace
- stacktrace du thread défaillant. Cette valeur est spécifiée uniquement si thread status == "failed"
.... = thread:get(time, metric)
Attend la fin du thread et renvoie le résultat de la fonction ou rien en cas d'erreur.
entrée : délai d'expiration de l'opération en termes de métriques de temps
sortie : résultats de l'invocation de la fonction capturée ou rien en cas d'erreur.
thread:wait(time, metric)
Attend la fin du thread et renvoie l'état du thread.
entrée : délai d'expiration de l'opération en termes de métriques de temps
sortie : renvoie l'état du thread. Le résultat est le même que thread:status()
thread:cancel(time, metric)
Interrompt l’exécution du thread. Une fois que cette fonction a été invoquée, l'indicateur « annulation » est défini et le thread peut être arrêté dans le futur (même après cet appel de fonction). Pour être sûr que le thread est arrêté, invoquez cette fonction avec un délai d'attente infini. L'annulation du thread terminé ne fera rien et renverra true
.
entrée : délai d'expiration de l'opération en termes de métriques de temps
sortie : renvoie true
si le thread a été arrêté ou false
.
thread:pause(time, metric)
Met le fil en pause. Une fois que cette fonction a été invoquée, l'indicateur « pause » est défini et le thread peut être mis en pause dans le futur (même après cet appel de fonction). Pour être sûr que le thread est en pause, invoquez cette fonction avec un délai d'attente infini.
entrée : délai d'expiration de l'opération en termes de métriques de temps
sortie : renvoie true
si le thread était en pause ou false
. Si le fil est terminé, la fonction retournera false
thread:resume()
Reprend le fil de discussion en pause. La fonction reprend le thread immédiatement s'il a été mis en pause. Cette fonction ne fait rien pour le thread terminé. La fonction n’a pas de paramètres d’entrée et de sortie.
id = effil.thread_id()
Donne un identifiant unique.
sortie : renvoie id
de chaîne unique pour le thread actuel .
effil.yield()
Point d'annulation explicite. La fonction vérifie les indicateurs d'annulation ou de pause du thread actuel et si cela est nécessaire, elle effectue les actions correspondantes (annuler ou mettre en pause le thread).
effil.sleep(time, metric)
Suspendre le fil de discussion en cours.
entrée : arguments de métriques de temps.
effil.hardware_threads()
Renvoie le nombre de threads simultanés pris en charge par l'implémentation. Fondamentalement, transmet la valeur de std::thread::hardware_concurrency.
sortie : nombre de threads matériels simultanés.
status, ... = effil.pcall(func, ...)
Fonctionne exactement de la même manière que pcall standard, sauf qu'il ne détectera pas l'erreur d'annulation de thread provoquée par l'appel thread:cancel().
saisir:
sortir:
true
si aucune erreur ne s'est produite, false
sinon effil.table
est un moyen d'échanger des données entre les threads effil. Il se comporte presque comme les tables Lua standard. Toutes les opérations avec une table partagée sont thread-safe. La table partagée stocke les types primitifs (nombre, booléen, chaîne), les fonctions, les tables, les données utilisateur légères et les données utilisateur basées sur effil. La table partagée ne stocke pas les threads Lua (coroutines) ni les données utilisateur arbitraires. Voir des exemples d'utilisation de tables partagées ici
Utilisez des tables partagées avec des tables normales . Si vous souhaitez stocker une table normale dans une table partagée, effil videra implicitement la table d'origine dans une nouvelle table partagée. Les tables partagées stockent toujours les sous-tables en tant que tables partagées.
Utilisez des tables partagées avec des fonctions . Si vous stockez la fonction dans une table partagée, effil vide implicitement cette fonction et l'enregistre sous forme de chaîne (et ses valeurs positives). Toutes les valeurs positives de la fonction seront capturées selon les règles suivantes.
table = effil.table(tbl)
Crée une nouvelle table partagée vide .
input : tbl
- est un paramètre facultatif , il ne peut s'agir que d'une table Lua normale dont les entrées seront copiées dans la table partagée.
sortie : nouvelle instance de table partagée vide. Il peut être vide ou non, selon le contenu tbl
.
table[key] = value
Définissez une nouvelle clé de table avec la valeur spécifiée.
saisir :
key
- toute valeur du type pris en charge. Voir la liste des types pris en chargevalue
- toute valeur du type pris en charge. Voir la liste des types pris en chargevalue = table[key]
Obtenez une valeur de la table avec la clé spécifiée.
input : key
- toute valeur du type pris en charge. Voir la liste des types pris en charge
sortie : value
- toute valeur du type pris en charge. Voir la liste des types pris en charge
tbl = effil.setmetatable(tbl, mtbl)
Définit une nouvelle métatable sur une table partagée. Semblable au standard setmetatable.
saisir :
tbl
doit être une table partagée pour laquelle vous souhaitez définir une métatable.mtbl
doit être une table normale ou une table partagée qui deviendra une métatable. S'il s'agit d'une table normale, effil créera une nouvelle table partagée et copiera tous les champs de mtbl
. Définissez mtbl
égal à nil
pour supprimer la métatable de la table partagée. sortie : renvoie simplement tbl
avec une nouvelle valeur métatable similaire à la méthode standard Lua setmetatable .
mtbl = effil.getmetatable(tbl)
Renvoie la métatable actuelle. Semblable au getmetatable standard
input : tbl
doit être une table partagée.
sortie : renvoie la métatable de la table partagée spécifiée. La table renvoyée a toujours le type effil.table
. La métatable par défaut est nil
.
tbl = effil.rawset(tbl, key, value)
Définissez l’entrée de la table sans appeler la métaméthode __newindex
. Similaire au rawset standard
saisir :
tbl
est une table partagée.key
- clé de la table à remplacer. La clé peut être de n’importe quel type pris en charge.value
- valeur à définir. La valeur peut être de n’importe quel type pris en charge. sortie : renvoie la même table partagée tbl
value = effil.rawget(tbl, key)
Obtient la valeur de la table sans appeler la métaméthode __index
. Semblable au rawget standard
saisir :
tbl
est une table partagée.key
- clé de table pour recevoir une valeur spécifique. La clé peut être de n’importe quel type pris en charge. sortie : renvoie value
requise stockée sous une key
spécifiée
effil.G
Est une table partagée prédéfinie globale. Cette table est toujours présente dans n'importe quel thread (n'importe quel état 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)
Transforme effil.table
en table Lua standard.
tbl = effil . table ({})
effil . type ( tbl ) -- 'effil.table'
effil . type ( effil . dump ( tbl )) -- 'table'
effil.channel
est un moyen d'échanger séquentiellement des données entre les threads effil. Il permet de pousser le message d'un fil et de le faire apparaître d'un autre. Le message du canal est un ensemble de valeurs de types pris en charge. Toutes les opérations avec les canaux sont thread-safe. Voir des exemples d'utilisation de canal ici
channel = effil.channel(capacity)
Crée une nouvelle chaîne.
entrée : capacité optionnelle du canal. Si capacity
est égale à 0
ou nil
la taille du canal est illimitée. La capacité par défaut est 0
.
sortie : renvoie une nouvelle instance de canal.
pushed = channel:push(...)
Pousse le message vers le canal.
input : n’importe quel nombre de valeurs de types pris en charge. Plusieurs valeurs sont considérées comme un message à canal unique, donc une simple pression sur le canal diminue la capacité de un.
sortie : pushed
est égal à true
si la ou les valeurs correspondent à la capacité du canal, false
sinon.
... = channel:pop(time, metric)
Message pop de la chaîne. Supprime les valeurs du canal et les renvoie. Si le canal est vide, attendez l'apparition d'une valeur.
input : délai d'attente en termes de métriques de temps (utilisé uniquement si le canal est vide).
sortie : quantité variable de valeurs qui ont été poussées par un seul appel channel:push().
size = channel:size()
Obtenez le nombre réel de messages dans le canal.
sortie : nombre de messages dans le canal.
Effil fournit un garbage collector personnalisé pour effil.table
et effil.channel
(et des fonctions avec des valeurs positives capturées). Il permet de gérer en toute sécurité les références cycliques pour les tables et les canaux dans plusieurs threads. Cependant, cela peut entraîner une utilisation supplémentaire de la mémoire. effil.gc
fournit un ensemble de méthodes pour configurer le garbage collector effil. Mais vous n’avez généralement pas besoin de le configurer.
Le garbage collector effectue son travail lorsque effil crée un nouvel objet partagé (table, canal et fonctions avec des valeurs positives capturées). Chaque itération GC vérifie la quantité d'objets. Si la quantité d'objets alloués devient supérieure, la valeur de seuil spécifique GC démarre le garbage collection. La valeur seuil est calculée comme previous_count * step
, où previous_count
- quantité d'objets lors de l'itération précédente ( 100 par défaut) et step
est un coefficient numérique spécifié par l'utilisateur ( 2,0 par défaut).
Par exemple : si step
GC est 2.0
et que la quantité d'objets alloués est 120
(restée après l'itération précédente du GC), alors GC commencera à collecter les déchets lorsque la quantité d'objets alloués sera égale à 240
.
Chaque thread est représenté comme un état Lua distinct avec son propre ramasse-miettes. Ainsi, les objets seront éventuellement supprimés. Les objets Effil lui-même sont également gérés par GC et utilisent la métaméthode __gc
userdata comme hook de désérialiseur. Pour forcer la suppression des objets :
collectgarbage()
dans tous les threads.effil.gc.collect()
dans n'importe quel thread.effil.gc.collect()
Forcer le garbage collection, mais cela ne garantit pas la suppression de tous les objets effil.
count = effil.gc.count()
Afficher le nombre de tables et de canaux partagés alloués.
sortie : renvoie le nombre actuel d'objets alloués. La valeur minimale est 1, effil.G
est toujours présent.
old_value = effil.gc.step(new_value)
Obtenir/définir le multiplicateur de pas de mémoire GC. La valeur par défaut est 2.0
. GC déclenche la collecte lorsque la quantité d'objets alloués augmente par step
.
input : new_value
est la valeur facultative de l’étape à définir. Si c'est nil
, la fonction renverra simplement une valeur actuelle.
sortie : old_value
est la valeur actuelle (si new_value == nil
) ou précédente (si new_value ~= nil
) de l'étape.
effil.gc.pause()
Mettez la CPG en pause. La collecte des déchets ne sera pas effectuée automatiquement. La fonction n'a aucune entrée ou sortie
effil.gc.resume()
Reprendre la CPG. Activez la collecte automatique des déchets.
enabled = effil.gc.enabled()
Obtenez l'état du GC.
sortie : renvoie true
si le garbage collection automatique est activé ou false
dans le cas contraire. Par défaut, renvoie true
.
size = effil.size(obj)
Renvoie le nombre d'entrées dans l'objet Effil.
input : obj
est une table ou un canal partagé.
sortie : nombre d'entrées dans la table partagée ou nombre de messages dans le canal
type = effil.type(obj)
Les threads, les canaux et les tables sont des données utilisateur. Ainsi, type()
renverra userdata
pour n'importe quel type. Si vous souhaitez détecter le type plus précisément, utilisez effil.type
. Il se comporte comme type()
normal, mais il peut détecter des données utilisateur spécifiques à Effil.
input : obj
est un objet de n’importe quel type.
sortie : nom de chaîne du type. Si obj
est un objet Effil, la fonction renvoie une chaîne comme effil.table
dans d'autres cas, elle renvoie le résultat de la fonction 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 "