35 votes

Quel paquetage multithreading pour Lua "fonctionne tout simplement" tel qu'il est livré ?

En codant en Lua, j'ai une boucle imbriquée en trois parties qui effectue 6000 itérations. Les 6000 itérations sont indépendantes et peuvent facilement être parallélisées. Quel paquetage de threads pour Lua compile dès le départ y obtient des vitesses parallèles décentes sur quatre cœurs ou plus ?

Voici ce que je sais pour l'instant :

  • luaproc vient de l'équipe de base de Lua, mais le paquet de logiciels sur luaforge est vieux, et la liste de diffusion a des rapports sur des défauts de fonctionnement. De plus, il n'est pas évident pour moi d'utiliser le modèle de passage de messages scalaires 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 font état de difficultés à construire ou à faire fonctionner les couloirs Lua. J'ai moi-même eu des difficultés à faire fonctionner le mécanisme de distribution sous-jacent "Lua rocks".

  • LuaThread nécessite un verrouillage explicite et exige que la communication entre les threads soit médié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 simultané fournit un modèle attrayant de passage de messages similaire à Erlang, mais il stipule que les processus ne partagent pas la mémoire. Il n'est pas clair si spawn fonctionne en fait avec tout Lua ou s'il existe des restrictions.

  • Russ Cox a proposé une enfilage occasionnel qui ne fonctionne que pour les fils C. Pas utile pour moi.

Je vais upvote toutes les réponses qui rapportent sur expérience réelle avec ces paquets 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

En run_test est passée en argument, donc un paquet ne peut m'être utile que s'il peut exécuter des fonctions arbitraires en parallèle. Mon objectif est un parallélisme suffisant pour obtenir une utilisation à 100% du CPU sur 6 à 8 cœurs.

3voto

Jeff Solinsky Points 11

Norman a écrit au sujet de luaproc :

"il n'est pas évident pour moi d'utiliser le modèle de passage de message scalaire pour obtenir des résultats en fin de compte dans un thread parent"

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

Pour obtenir la fonctionnalité souhaitée, 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 fonctionnant pas à partir du planificateur luaproc. De plus, mes modifications permettent d'utiliser luaproc send/receive à partir de coroutines créées à partir de luaproc.newproc() et d'états lua créés.

J'ai ajouté une fonction supplémentaire luaproc.addproc() à l'api qui doit être appelée depuis n'importe quel état lua s'exécutant depuis un contexte non contrôlé par le planificateur luaproc afin de se configurer avec luaproc pour envoyer/recevoir des messages.

J'envisage de publier les sources en tant que nouveau projet github ou de contacter les développeurs pour voir s'ils souhaitent reprendre mes ajouts. Les suggestions sur la façon dont je devrais le mettre à la disposition des autres sont les bienvenues.

2voto

Yin Zhu Points 10438

Vérifiez le fils bibliothèque dans la famille des torches. Elle implémente un modèle de pool de threads : quelques vrais threads (pthread sous linux et Windows thread sous win32) sont créés en premier. Chaque thread possède un objet lua_State et une file d'attente de tâches bloquante qui admet les tâches ajoutées par le thread principal.

Les objets Lua sont copiés du fil principal vers le fil de travail. Cependant, les objets C tels que Tenseurs de torche o tds Les structures de données peuvent être transmises aux threads de travail par le biais de pointeurs - c'est ainsi que l'on obtient une mémoire partagée limitée.

2voto

Alexander Gladysh Points 9554

Je me rends compte que ce n'est pas une solution prête à l'emploi, mais, peut-être aller à l'ancienne et jouer avec des fourches ? (En supposant que vous êtes sur un système POSIX).

Ce que j'aurais fait :

  • Juste avant votre boucle, mettez tous les tests dans une file d'attente, accessible entre les processus. (Un fichier, un Redis LIST ou tout ce que vous préférez).

  • Toujours avant la boucle, générez plusieurs forks avec lua-posix (comme le nombre de cœurs, voire plus selon la nature des tests). Dans le fork parent, attendez que tous les enfants quittent.

  • Dans chaque fork d'une boucle, récupérer un test dans la file d'attente, l'exécuter, mettre les résultats quelque part. (Dans un fichier, dans un Redis LIST, où vous voulez.) 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 des tests comme vous le faites actuellement.

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 qu'il devrait être assez facile de contourner ce problème.

2voto

frooyo Points 658

C'est un parfait exemple de MapReduce

Vous pouvez utiliser LuaRings pour répondre à vos besoins de parallélisation.

1voto

Norman Ramsey Points 115730

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 fils parallèles sont lancés, d'après ce que je sais. il n'y a aucun moyen pour eux de communiquer en retour avec le parent. Cette propriété était le gros morceau pour moi. J'ai fini par comprendre la marche à suivre : lorsqu'il a fini de bifurquer des threads, le parent s'arrête et attend. Le travail qui aurait été fait par le parent doit être fait par un thread enfant, qui doit être dédié à ce travail. Ce n'est pas un modèle génial, mais il fonctionne.

  • La communication entre les parents et les enfants est très limitée . Le parent ne peut communiquer que des valeurs scalaires : chaînes de caractères, booléens et nombres. Si le parent veut communiquer des valeurs plus complexes, comme des tableaux et des fonctions, il doit les coder en tant que chaînes de caractères. Ce codage peut s'effectuer 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 à l'aide de la fonction require .

  • Les enfants n'héritent de rien de l'environnement des parents. En particulier, ils n'héritent pas package.path o package.cpath . J'ai dû contourner ce problème par la façon dont j'ai écrit le code pour les enfants.

  • Le moyen le plus pratique de communiquer entre parent et enfant est de définir l'enfant comme une fonction, et de faire en sorte que l'enfant capture les informations parentales dans ses variables libres, connues dans le langage Lua sous le nom de "upvalues". Ces variables libres ne peuvent pas être des variables globales, et elles doivent être des scalaires. Néanmoins, 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é comme, par exemple,

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

    Cet appel est la façon dont les valeurs randoms y workers sont communiquées de parent à enfant.

    L'assertion est essentielle ici, comme si vous oubliez les règles et capturez accidentellement un tableau ou une fonction locale, luaproc.newproc échouera.

Une fois que j'ai compris ces propriétés, luaproc a effectivement fonctionné "tout de suite", lorsque téléchargé depuis askyrme sur github .

ETA : Il y a un limitation gênante dans certaines circonstances, l'appel fread() dans un thread peut empêcher d'autres threads d'être programmés. En particulier, si j'exécute la séquence

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

el read opération bloque tous les autres fils . Je ne sais pas pourquoi - je suppose qu'il s'agit d'une absurdité de la glibc. La solution de contournement que j'ai utilisée était d'appeler directement à read(2) ce qui a nécessité un peu de code de collage, mais cela fonctionne correctement avec io.popen y file:close() .

Il y a une autre limitation qui mérite d'être notée :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X