Question Quel paquet multithreading pour Lua "fonctionne juste" comme livré?


En codant en Lua, j'ai une boucle à trois nids qui traverse 6000 itérations. Toutes les 6000 itérations sont indépendantes et peuvent facilement être parallélisées. Quel paquet de threads pour Lua compile hors de la boîte et obtient des accélérations parallèles décentes sur quatre ou plusieurs noyaux?

Voici ce que je sais jusqu'ici:

  • luaproc vient de l'équipe de base de Lua, mais le lot de logiciels sur luaforge est ancien, et la liste de diffusion en a signalé des erreurs. De plus, il n’est pas évident pour moi d’utiliser le modèle de transmission de messages scalaire pour obtenir des résultats dans un thread parent.

  • Lua Lanes fait des déclarations intéressantes mais semble être une solution lourde et complexe. De nombreux messages sur la liste de diffusion indiquent que Lua Lanes a du mal à construire ou à travailler pour eux. J'ai moi-même eu du mal à faire fonctionner le mécanisme de distribution "Lua rocks" sous-jacent pour moi.

  • LuaThread nécessite un verrouillage explicite et exige que la communication entre les threads soit assurée par des variables globales protégées par des verrous. Je pourrais imaginer pire, mais je serais plus heureux avec un niveau d'abstraction plus élevé.

  • Lua concomitant fournit un modèle de transmission de messages attractif similaire à Erlang, mais dit que les processus ne partagent pas la mémoire. On ne sait pas si spawn travaille en fait avec tout Lua fonctionne ou s'il y a des restrictions.

  • Russ Cox a proposé un filetage occasionnel modèle qui ne fonctionne que pour les threads C. Pas utile pour moi

Je soulèverai toutes les réponses sur le rapport expérience réelle avec ceux-ci ou tout autre paquet multithreading, ou toute réponse qui fournit de nouvelles informations.


Pour référence, voici la boucle que je voudrais paralléliser:

for tid, tests in pairs(tests) do
  local results = { }
  matrix[tid] = results
  for i, test in pairs(tests) do
    if test.valid then
      results[i] = { }
      local results = results[i]
      for sid, bin in pairs(binaries) do
        local outcome, witness = run_test(test, bin)
        results[sid] = { outcome = outcome, witness = witness }
      end
    end
  end
end

le run_test Si la fonction est passée en argument, un paquetage ne peut m'être utile que s'il peut exécuter des fonctions arbitraires en parallèle. Mon objectif est assez de parallélisme pour obtenir 100% d'utilisation du processeur sur 6 à 8 cœurs.


34
2018-04-16 20:53


origine


Réponses:


Norman a écrit concernant luaproc:

"Il n'est pas évident pour moi d'utiliser le modèle de transmission de messages scalaire pour obtenir des résultats dans un thread parent"

J'ai eu le même problème avec un cas d'utilisation que je traitais. J'ai aimé lua proc en raison de son implémentation simple et légère, mais mon cas d'utilisation comportait le code C qui appelait lua, ce qui provoquait une co-routine qui devait envoyer / recevoir des messages pour interagir avec d'autres threads luaproc.

Pour atteindre mes fonctionnalités souhaitées, j'ai dû ajouter des fonctionnalités à luaproc pour permettre l'envoi et la réception de messages à partir du thread parent ou de tout autre thread ne provenant pas du planificateur luaproc. De plus, mes modifications permettent d'utiliser luaproc send / receive depuis les coroutines créées à partir des états lua créés avec luaproc.newproc ().

J'ai ajouté une fonction luaproc.addproc () à l'API qui doit être appelée à partir de n'importe quel état lu à partir d'un contexte non contrôlé par le programmateur luaproc afin de s'installer avec luaproc pour envoyer / recevoir des messages.

J'envisage de publier la source sous la forme d'un nouveau projet github ou de contacter les développeurs et de voir s'ils souhaitent retirer mes ajouts. Les suggestions sur la manière dont je devrais le mettre à la disposition des autres sont les bienvenues.


2
2017-09-07 15:04



Vérifier la des fils bibliothèque dans la famille des flambeaux. Il implémente un modèle de pool de threads: quelques vrais threads (pthread dans linux et windows thread dans win32) sont créés en premier. Chaque thread a un objet lua_State et une file d'attente de jobs bloquante qui admet les travaux ajoutés à partir du thread principal.

Les objets Lua sont copiés du thread principal vers le thread de travail. Cependant les objets C tels que Tenseurs torche ou Tds les structures de données peuvent être transmises aux tâches via des pointeurs - c'est ainsi que la mémoire partagée est limitée.


2
2017-07-23 13:48



Ceci est un exemple parfait de MapReduce

Vous pouvez utiliser LuaRings pour accomplir vos besoins de parallélisation.


1
2018-05-23 03:09



Lua concurrente peut sembler être la voie à suivre, mais comme je le note dans mes mises à jour ci-dessous, il ne lance pas les choses en parallèle. L'approche que j'ai essayée était de générer plusieurs processus qui exécutent des fermetures décapées reçues par la file d'attente de messages.

Mettre à jour

Lua concurrente semble gérer des fonctions de première classe et des fermetures sans anicroche. Voir l'exemple de programme suivant.

require 'concurrent'

local NUM_WORKERS = 4       -- number of worker threads to use
local NUM_WORKITEMS = 100   -- number of work items for processing

-- calls the received function in the local thread context
function worker(pid)
    while true do
        -- request new work
        concurrent.send(pid, { pid = concurrent.self() })
        local msg = concurrent.receive()

        -- exit when instructed
        if msg.exit then return end

        -- otherwise, run the provided function
        msg.work()
    end
end

-- creates workers, produces all the work and performs shutdown
function tasker()
    local pid = concurrent.self()

    -- create the worker threads
    for i = 1, NUM_WORKERS do concurrent.spawn(worker, pid) end

    -- provide work to threads as requests are received
    for i = 1, NUM_WORKITEMS do
        local msg = concurrent.receive()

        -- send the work as a closure
        concurrent.send(msg.pid, { work = function() print(i) end, pid = pid })
    end

    -- shutdown the threads as they complete
    for i = 1, NUM_WORKERS do
        local msg = concurrent.receive()
        concurrent.send(msg.pid, { exit = true })
    end
end

-- create the task process
local pid = concurrent.spawn(tasker)

-- run the event loop until all threads terminate
concurrent.loop()

Mise à jour 2

Grattez toutes ces choses ci-dessus. Quelque chose ne semblait pas correct quand je testais cela. Il s'avère que Lua concurrente n'est pas du tout concurrente. Les "processus" sont implémentés avec des coroutines et tous s'exécutent en coopération dans le même contexte de thread. C'est ce que nous obtenons pour ne pas lire attentivement!

Donc, au moins, j'ai éliminé l'une des options que je suppose. :(


1
2018-05-23 03:44



Je me rends compte que ce n’est pas une solution pratique, mais peut-être aller à la vieille école et jouer avec les fourchettes? (En supposant que vous êtes sur un système POSIX.)

Ce que j'aurais fait:

  • Juste avant votre boucle, placez tous les tests dans une file d'attente, accessible entre les processus. (Un fichier, une liste Redis ou tout ce que vous aimez le plus.)

  • Aussi avant la boucle, engendrer plusieurs fourches avec lua-posix (identique au nombre de cœurs ou même plus en fonction de la nature des tests). Dans la fourchette parent attendez que tous les enfants cessent de fumer.

  • Dans chaque fourchette d'une boucle, obtenez un test de la file d'attente, exécutez-le, placez les résultats quelque part. (À un fichier, à une liste Redis, n'importe où ailleurs.) S'il n'y a plus de tests dans la file d'attente, quittez.

  • Dans le parent, récupérez et traitez tous les résultats de test comme vous le faites maintenant.

Cela suppose que les paramètres de test et les résultats sont sérialisables. Mais même s’ils ne le sont pas, je pense que c’est plutôt facile de tromper autour de ça.


1
2018-04-16 23:32



J'ai maintenant construit une application parallèle en utilisant luaproc. Voici quelques idées fausses qui m'ont empêché de l'adopter plus tôt et comment les contourner.

  • Une fois que les threads parallèles sont lancés, autant que je sache il leur est impossible de communiquer avec le parent.  Cette propriété était le gros bloc pour moi. Finalement, j'ai compris la voie à suivre: quand il a fini de truquer les threads, le parent s'arrête et attend. Le travail qui aurait été effectué par le parent devrait plutôt être effectué par un thread enfant, qui devrait être dédié à ce travail. Pas un bon modèle, mais ça marche.

  • La communication entre parents et enfants est très limitée. Le parent ne peut communiquer que des valeurs scalaires: chaînes, booléens et nombres. Si le parent veut communiquer des valeurs plus complexes, comme des tables et des fonctions, il doit les coder en tant que chaînes. Un tel codage peut avoir lieu en ligne dans le programme, ou (surtout) les fonctions peuvent être parquées dans le système de fichiers et chargées dans l’enfant en utilisant require.

  • Les enfants n'héritent de rien de l'environnement des parents.  En particulier, ils n'héritent pas package.path ou package.cpath. J'ai dû contourner ce problème en écrivant le code pour les enfants.

  • La manière la plus commode de communiquer de parent à enfant est de définir l’enfant comme une fonction et de demander à l’enfant de saisir l’information parentale dans ses variables libres, appelées dans le jargon Lua «valeurs supérieures». Ces variables libres peuvent ne pas être des variables globales et doivent être des scalaires. Pourtant, c'est un modèle décent. Voici un exemple:

    local function spawner(N, workers)
      return function()
        local luaproc = require 'luaproc'
        for i = 1, N do
          luaproc.send('source', i)
        end
        for i = 1, workers do
          luaproc.send('source', nil)
        end
      end
    end
    

    Ce code est utilisé par exemple comme

    assert(luaproc.newproc(spawner(randoms, workers)))
    

    Cet appel est comment les valeurs randoms et workers sont communiquées du parent à l'enfant.

    L'affirmation est essentielle ici, comme si vous oubliez les règles et capturez accidentellement une table ou une fonction locale, luaproc.newproc va échouer.

Une fois que j'ai compris ces propriétés, luaproc a effectivement travaillé "out of the box", quand téléchargé depuis askyrme sur github.

ETA: Il y a un limitation gênante: dans certaines circonstances, appeler fread() dans un thread peut empêcher d'autres threads d'être planifiés. En particulier, si je lance la séquence

local file = io.popen(command, 'r')
local result = file:read '*a'
file:close()
return result

la read opération bloque tous les autres threads. Je ne sais pas pourquoi c'est --- Je suppose que c'est un non-sens qui se passe au sein de la glibc. La solution de contournement que j'ai utilisée consistait à appeler directement read(2), qui nécessitait un peu de code de colle, mais cela fonctionne correctement avec io.popen et file:close().

Il y a une autre limitation à noter:

  • Contrairement à la conception originale de Tony Hoare sur le traitement séquentiel communicant, et contrairement aux implémentations sérieuses et matures du passage synchrone des messages, luaproc ne permet pas à un récepteur de bloquer simultanément plusieurs canaux. Cette limitation est grave et exclut de nombreux modèles de conception que le passage synchrone des messages est bon, mais on trouve encore de nombreux modèles simples de parallélisme, en particulier le type "parbegin" que je devais résoudre pour mon problème initial.

1
2018-03-21 14:28