web-dev-qa-db-fra.com

Comment utiliser le multitraitement avec des instances de classe en Python?

J'essaie de créer une classe qui peut exécuter un processus distinct pour effectuer un travail qui prend beaucoup de temps, en lancer une série à partir d'un module principal, puis attendre que toutes les tâches soient terminées. Je veux lancer les processus une fois, puis continuer à les nourrir, plutôt que de créer et de détruire des processus. Par exemple, peut-être que j'ai 10 serveurs exécutant la commande dd, puis je veux tous les scp un fichier, etc. 

Mon but ultime est de créer une classe pour chaque système qui garde la trace des informations du système auquel elle est liée, telles que l'adresse IP, les journaux, le temps d'exécution, etc. Mais cette classe doit pouvoir lancer une commande système, puis renvoyer exécution en retour à l'appelant pendant l'exécution de la commande système, pour un suivi ultérieur du résultat de la commande système.

Ma tentative échoue car je ne peux pas envoyer une méthode d'instance d'une classe sur le canal au sous-processus via pickle. Ceux ne sont pas picklable. J'ai donc essayé de le réparer de différentes façons mais je n'arrive pas à le comprendre. Comment mon code peut-il être corrigé pour faire cela? À quoi sert le multitraitement si vous ne pouvez rien envoyer d'utile?

Existe-t-il une bonne documentation sur le multitraitement utilisé avec les instances de classe? Le seul moyen de faire fonctionner le module de multitraitement consiste à utiliser des fonctions simples. Toute tentative d'utilisation dans une instance de classe a échoué. Peut-être que je devrais passer des événements à la place? Je ne comprends pas encore comment faire ça.

import multiprocessing
import sys
import re

class ProcessWorker(multiprocessing.Process):
    """
    This class runs as a separate process to execute worker's commands in parallel
    Once launched, it remains running, monitoring the task queue, until "None" is sent
    """

    def __init__(self, task_q, result_q):
        multiprocessing.Process.__init__(self)
        self.task_q = task_q
        self.result_q = result_q
        return

    def run(self):
        """
        Overloaded function provided by multiprocessing.Process.  Called upon start() signal
        """
        proc_name = self.name
        print '%s: Launched' % (proc_name)
        while True:
            next_task_list = self.task_q.get()
            if next_task is None:
                # Poison pill means shutdown
                print '%s: Exiting' % (proc_name)
                self.task_q.task_done()
                break
            next_task = next_task_list[0]
            print '%s: %s' % (proc_name, next_task)
            args = next_task_list[1]
            kwargs = next_task_list[2]
            answer = next_task(*args, **kwargs)
            self.task_q.task_done()
            self.result_q.put(answer)
        return
# End of ProcessWorker class

class Worker(object):
    """
    Launches a child process to run commands from derived classes in separate processes,
    which sit and listen for something to do
    This base class is called by each derived worker
    """
    def __init__(self, config, index=None):
        self.config = config
        self.index = index

        # Launce the ProcessWorker for anything that has an index value
        if self.index is not None:
            self.task_q = multiprocessing.JoinableQueue()
            self.result_q = multiprocessing.Queue()

            self.process_worker = ProcessWorker(self.task_q, self.result_q)
            self.process_worker.start()
            print "Got here"
            # Process should be running and listening for functions to execute
        return

    def enqueue_process(target):  # No self, since it is a decorator
        """
        Used to place an command target from this class object into the task_q
        NOTE: Any function decorated with this must use fetch_results() to get the
        target task's result value
        """
        def wrapper(self, *args, **kwargs):
            self.task_q.put([target, args, kwargs]) # FAIL: target is a class instance method and can't be pickled!
        return wrapper

    def fetch_results(self):
        """
        After all processes have been spawned by multiple modules, this command
        is called on each one to retreive the results of the call.
        This blocks until the execution of the item in the queue is complete
        """
        self.task_q.join()                          # Wait for it to to finish
        return self.result_q.get()                  # Return the result

    @enqueue_process
    def run_long_command(self, command):
        print "I am running number % as process "%number, self.name

        # In here, I will launch a subprocess to run a  long-running system command
        # p = Popen(command), etc
        # p.wait(), etc
        return 

    def close(self):
        self.task_q.put(None)
        self.task_q.join()

if __== '__main__':
    config = ["some value", "something else"]
    index = 7
    workers = []
    for i in range(5):
        worker = Worker(config, index)
        worker.run_long_command("ls /")
        workers.append(worker)
    for worker in workers:
        worker.fetch_results()

    # Do more work... (this would actually be done in a distributor in another class)

    for worker in workers:
        worker.close() 

Edit: j'ai essayé de déplacer la classe ProcessWorker et la création des files d'attente de multitraitement en dehors de la classe Worker, puis j'ai essayé de décaper manuellement l'instance de travailleur. Même cela ne fonctionne pas et j'obtiens une erreur 

RuntimeError: les objets de la file d'attente doivent uniquement être partagés entre les processus par héritage

. Mais je ne fais que transmettre les références de ces files d'attente à l'instance de travail ?? Il me manque quelque chose de fondamental. Voici le code modifié de la section principale:

if __== '__main__':
    config = ["some value", "something else"]
    index = 7
    workers = []
    for i in range(1):
        task_q = multiprocessing.JoinableQueue()
        result_q = multiprocessing.Queue()
        process_worker = ProcessWorker(task_q, result_q)
        worker = Worker(config, index, process_worker, task_q, result_q)
        something_to_look_at = pickle.dumps(worker) # FAIL:  Doesn't like queues??
        process_worker.start()
        worker.run_long_command("ls /")
22
David Lynch

Au lieu de tenter d’envoyer une méthode elle-même (ce qui n’est pas pratique), essayez d’envoyer un name d’une méthode à exécuter.

À condition que chaque utilisateur exécute le même code, il s'agit d'une simple getattr(self, task_name).

Je transmettrais des tuples (task_name, task_args), où task_args était un dicton à introduire directement dans la méthode de la tâche:

next_task_name, next_task_args = self.task_q.get()
if next_task_name:
  task = getattr(self, next_task_name)
  answer = task(**next_task_args)
  ...
else:
  # poison pill, shut down
  break
8
9000

Le problème était donc que je supposais que Python faisait une sorte de magie qui diffère quelque peu de la façon dont fonctionne C++/fork (). Je pensais en quelque sorte que Python ne faisait que copier la classe, pas tout le programme dans un processus séparé. J'ai sérieusement perdu des jours à essayer de faire fonctionner cela parce que toutes les discussions sur la sérialisation des cornichons me faisaient penser que cela envoyait tout par la voie des pistes. Je savais que certaines choses ne pouvaient pas être acheminées par le tuyau, mais je pensais que mon problème était que je ne les emballais pas correctement.

Tout cela aurait pu être évité si la documentation Python m'avait donné une vue de 10 000 pieds sur ce qui se passe lorsque ce module est utilisé. Bien sûr, il me dit ce que font les méthodes du module multiprocessus et me donne quelques exemples de base, mais ce que je veux savoir, c'est quelle est la "théorie de l'opération" dans les coulisses! Voici le genre d'informations que j'aurais pu utiliser. S'il vous plaît, inscrivez-vous si ma réponse est désactivée. Cela m'aidera à apprendre.

Lorsque vous lancez un processus à l'aide de ce module, l'ensemble du programme est copié dans un autre processus. Mais comme ce n’est pas le processus "__main__" et que mon code le vérifiait, il ne déclenche pas encore un autre processus à l’infini. Il s’arrête et reste assis à attendre quelque chose à faire, comme un zombie. Tout ce qui a été initialisé dans le parent au moment de l'appel de multiprocess.Process () est configuré et prêt à fonctionner. Une fois que vous avez mis quelque chose dans le multiprocess.Queue ou dans la mémoire partagée, ou dans un tuyau, etc. (quelle que soit votre communication), le processus séparé le reçoit et se met au travail. Il peut s’appuyer sur tous les modules et configurations importés comme si c’était le parent. Cependant, lorsque certaines variables d'état internes changent dans le processus parent ou séparé, ces modifications sont isolées. Une fois le processus créé, il vous appartient maintenant de les synchroniser si nécessaire, via une file d'attente, un canal, la mémoire partagée, etc.

J'ai jeté le code et recommencé, mais maintenant je ne crée qu'une fonction supplémentaire dans la variable ProcessWorker, une méthode "execute" qui exécute une ligne de commande. Assez simple. Je n'ai pas à m'inquiéter de lancer puis de fermer un tas de processus de cette façon, ce qui m'a causé toutes sortes d'instabilités et de problèmes de performances dans le passé en C++. Lorsque je suis passé au lancement des processus au début, puis que je transmettais des messages à ces processus en attente, mes performances se sont améliorées et elles étaient très stables.

BTW, j'ai regardé ce lien pour obtenir de l'aide, ce qui m'a mis en rogne parce que l'exemple m'a fait penser que des méthodes étaient transportées à travers les files d'attente: http://www.doughellmann.com/PyMOTW/multiprocessing/communication.html Le deuxième exemple de la première section utilisait "next_task ()" qui (s’avérait pour moi) en train d’exécuter une tâche reçue via la file d’attente.

20
David Lynch