web-dev-qa-db-fra.com

Python Manager dict dans Multiprocessing

Voici un code de multiprocesseur simple:

from multiprocessing import Process, Manager

manager = Manager()
d = manager.dict()

def f():
    d[1].append(4)
    print d

if __name__ == '__main__':
    d[1] = []
    p = Process(target=f)
    p.start()
    p.join()

La sortie que j'obtiens est:

{1: []}

Pourquoi je ne reçois pas {1: [4]} comme sortie?

24
Bruce

Voici ce que vous avez écrit:

# from here code executes in main process and all child processes
# every process makes all these imports
from multiprocessing import Process, Manager

# every process creates own 'manager' and 'd'
manager = Manager() 
# BTW, Manager is also child process, and 
# in its initialization it creates new Manager, and new Manager
# creates new and new and new
# Did you checked how many python processes were in your system? - a lot!
d = manager.dict()

def f():
    # 'd' - is that 'd', that is defined in globals in this, current process 
    d[1].append(4)
    print d

if __name__ == '__main__':
# from here code executes ONLY in main process 
    d[1] = []
    p = Process(target=f)
    p.start()
    p.join()

Voici ce que vous auriez dû écrire:

from multiprocessing import Process, Manager
def f(d):
    d[1] = d[1] + [4]
    print d

if __name__ == '__main__':
    manager = Manager() # create only 1 mgr
    d = manager.dict() # create only 1 dict
    d[1] = []
    p = Process(target=f,args=(d,)) # say to 'f', in which 'd' it should append
    p.start()
    p.join()
31
akaRem

La raison pour laquelle le nouvel élément ajouté à d[1] N'est pas imprimé est indiquée dans la documentation officielle de Python :

Les modifications apportées aux valeurs ou éléments mutables dans les proxys dict et list ne seront pas propagées via le gestionnaire, car le proxy n'a aucun moyen de savoir quand ses valeurs ou éléments sont modifiés. Pour modifier un tel élément, vous pouvez réaffecter l'objet modifié au proxy de conteneur.

Par conséquent, c'est en fait ce qui se passe:

from multiprocessing import Process, Manager

manager = Manager()
d = manager.dict()

def f():
    # invoke d.__getitem__(), returning a local copy of the empty list assigned by the main process,
    # (consider that a KeyError exception wasn't raised, so a list was definitely returned),
    # and append 4 to it, however this change is not propagated through the manager,
    # as it's performed on an ordinary list with which the manager has no interaction
    d[1].append(4)
    # convert d to string via d.__str__() (see https://docs.python.org/2/reference/datamodel.html#object.__str__),
    # returning the "remote" string representation of the object (see https://docs.python.org/2/library/multiprocessing.html#multiprocessing.managers.SyncManager.list),
    # to which the change above was not propagated
    print d

if __name__ == '__main__':
    # invoke d.__setitem__(), propagating this assignment (mapping 1 to an empty list) through the manager
    d[1] = []
    p = Process(target=f)
    p.start()
    p.join()

Réaffecter d[1] Avec une nouvelle liste, ou même avec la même liste une fois de plus, après sa mise à jour, déclenche le gestionnaire pour propager le changement:

from multiprocessing import Process, Manager

manager = Manager()
d = manager.dict()

def f():
    # perform the exact same steps, as explained in the comments to the previous code snippet above,
    # but in addition, invoke d.__setitem__() with the changed item in order to propagate the change
    l = d[1]
    l.append(4)
    d[1] = l
    print d

if __name__ == '__main__':
    d[1] = []
    p = Process(target=f)
    p.start()
    p.join()

La ligne d[1] += [4] Aurait également fonctionné.


Alternativement, Depuis Python 3.6 , par ce changeset suivant ce problème , il est également possible de tilisez des objets proxy imbriqués qui propagent automatiquement toutes les modifications effectuées sur eux à l'objet proxy contenant. Ainsi, remplacer la ligne d[1] = [] Par d[1] = manager.list() corrigerait également le problème:

from multiprocessing import Process, Manager

manager = Manager()
d = manager.dict()

def f():
    d[1].append(4)
    # the __str__() method of a dict object invokes __repr__() on each of its items,
    # so explicitly invoking __str__() is required in order to print the actual list items
    print({k: str(v) for k, v in d.items()}

if __name__ == '__main__':
    d[1] = manager.list()
    p = Process(target=f)
    p.start()
    p.join()

Malheureusement, ce correctif n'a pas été porté sur Python 2.7 (à partir de Python 2.7.13).


REMARQUE (fonctionnant sous le système d'exploitation Windows):

Bien que le comportement décrit s'applique également au système d'exploitation Windows, les extraits de code joints échoueraient lors de leur exécution sous Windows en raison de processus différent mécanisme de création , qui repose sur l'API CreateProcess() plutôt que sur l'appel système fork() , qui n'est pas pris en charge.

Chaque fois qu'un nouveau processus est créé via le module multiprocessing, Windows crée un nouveau processus d'interpréteur Python qui importe le module principal , avec des effets secondaires potentiellement dangereux. Afin de contourner ce problème, la directive de programmation suivante est recommandé :

Assurez-vous que le module principal peut être importé en toute sécurité par un nouvel interpréteur - Python sans provoquer d'effets secondaires involontaires (comme le démarrage d'un nouveau processus).

Par conséquent, l'exécution des extraits de code joints tels qu'ils se trouvent sous Windows tenterait de créer un nombre infini de processus en raison de la ligne manager = Manager(). Cela peut être facilement résolu en créant les objets Manager et Manager.dict À l'intérieur de la clause if __name__ == '__main__' Et en passant l'objet Manager.dict Comme argument à f(), comme dans cette réponse .

Plus de détails sur le problème peuvent être trouvés dans cette réponse .

13
Yoel

Je pense que c'est un bug dans les appels proxy du gestionnaire. Vous pouvez éviter les méthodes d'appel de liste partagée, comme:

from multiprocessing import Process, Manager

manager = Manager()
d = manager.dict()

def f():
    # get the shared list
    shared_list = d[1]

    shared_list.append(4)

    # forces the shared list to 
    # be serialized back to manager
    d[1] = shared_list

    print d

if __name__ == '__main__':
    d[1] = []
    p = Process(target=f)
    p.start()
    p.join()

    print d
12
Carlo Pires
from multiprocessing import Process, Manager
manager = Manager()
d = manager.dict()
l=manager.list()

def f():
    l.append(4)
    d[1]=l
    print d

if __name__ == '__main__':
    d[1]=[]
    p = Process(target=f)
    p.start()
    p.join()
2
zz yzz