multiprocessing: come condivido un dict tra più processi?


113

Un programma che crea diversi processi che funzionano su una coda collegabile Qe può eventualmente manipolare un dizionario globale Dper memorizzare i risultati. (quindi ogni processo figlio può utilizzare Dper memorizzare il suo risultato e vedere anche quali risultati stanno producendo gli altri processi figlio)

Se stampo il dizionario D in un processo figlio, vedo le modifiche che sono state fatte su di esso (cioè su D). Ma dopo che il processo principale si unisce a Q, se stampo D, è un dict vuoto!

Capisco che sia un problema di sincronizzazione / blocco. Qualcuno può dirmi cosa sta succedendo qui e come posso sincronizzare l'accesso a D?


1
Questo non funziona come previsto almeno su python 3.7.2 usando osx 10.14.4 Dict non è sincronizzato e il suo contenuto viene riscritto da altri processi. Tuttavia, <code> multiprocessing.Manager (). List () </code> funziona come previsto.
Andrew Druchenko

Risposte:


162

Una risposta generale implica l'utilizzo di un Manageroggetto. Adattato dai documenti:

from multiprocessing import Process, Manager

def f(d):
    d[1] += '1'
    d['2'] += 2

if __name__ == '__main__':
    manager = Manager()

    d = manager.dict()
    d[1] = '1'
    d['2'] = 2

    p1 = Process(target=f, args=(d,))
    p2 = Process(target=f, args=(d,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()

    print d

Produzione:

$ python mul.py 
{1: '111', '2': 6}

4
Grazie mittente. In effetti, D = multiprocessing.Manager (). Dict () risolve il mio problema. Stavo usando D = dict ().
dop

3
@LorenzoBelli, se stai chiedendo se l'accesso al gestore è sincronizzato, credo che la risposta sia si. multiprocessing.Manager()restituisce un'istanza diSyncManager , il cui nome suggerisce altrettanto!
mittente

@senderle Voglio condividere lo stato casuale numpy di un processo genitore con un processo figlio. Ho provato a usare Managerma ancora senza fortuna. Potresti dare un'occhiata alla mia domanda qui e vedere se puoi offrire una soluzione? Posso ancora ottenere numeri casuali diversi se lo faccio np.random.seed(None)ogni volta che generi un numero casuale, ma questo non mi consente di utilizzare lo stato casuale del processo genitore, che non è quello che voglio. Qualsiasi aiuto è molto apprezzato.
Amir

1
@RadioControlled felice di scrivere un aggiornamento, ma brevemente, anche se non penso che tu possa farlo accadere direttamente, puoi facilmente creare un nuovo dict gestito con le stesse chiavi e valori e usarlo al posto dell'originale. È adeguato per il tuo caso?
senderle

1
@senderle, è quello che ho finito per fare. Quindi la risposta sarebbe che dovresti fare proprio questo.
Radiocontrollato il

25

il multiprocessing non è come il threading. Ogni processo figlio riceverà una copia della memoria del processo principale. In genere lo stato è condiviso tramite comunicazione (pipe / prese), segnali o memoria condivisa.

Il multiprocessing rende disponibili alcune astrazioni per il tuo caso d'uso: lo stato condiviso che viene trattato come locale tramite l'uso di proxy o memoria condivisa: http://docs.python.org/library/multiprocessing.html#sharing-state-between-processes

Sezioni rilevanti:


1
Molte grazie. Mi hai portato alla / a soluzione: multiprocessing.Manager (). Dict ().
dop

Qualcuno può spiegare cosa significa l'affermazione "Ogni processo figlio riceverà una copia della memoria del processo principale".
Itsme2003

@ Itsme2003 per impostazione predefinita un processo generato non ha accesso alla memoria del processo genitore (questa è una delle principali differenze rispetto ai thread). Quindi, quando un processo necessita di un oggetto del processo genitore, deve crearne una copia (invece di ottenere un riferimento all'oggetto reale). La risposta sopra elabora su come condividere oggetti tra i processi.
Niklas Mertsch

Perché questo è spesso sbagliato: finché non si modifica l'oggetto, almeno nella normale configurazione di Linux, l'oggetto verrà effettivamente memorizzato solo una volta nella memoria. Verrà copiato non appena verrà modificato. Questo può essere molto importante se è necessario risparmiare memoria e non modificare l'oggetto.
Radiocontrollato il

16

Vorrei condividere il mio lavoro che è più veloce del dict di Manager ed è più semplice e più stabile della libreria pyshmht che utilizza tonnellate di memoria e non funziona per Mac OS. Anche se il mio dict funziona solo per corde normali ed è attualmente immutabile. Uso l'implementazione del sondaggio lineare e memorizzo chiavi e coppie di valori in un blocco di memoria separato dopo la tabella.

from mmap import mmap
import struct
from timeit import default_timer
from multiprocessing import Manager
from pyshmht import HashTable


class shared_immutable_dict:
    def __init__(self, a):
        self.hs = 1 << (len(a) * 3).bit_length()
        kvp = self.hs * 4
        ht = [0xffffffff] * self.hs
        kvl = []
        for k, v in a.iteritems():
            h = self.hash(k)
            while ht[h] != 0xffffffff:
                h = (h + 1) & (self.hs - 1)
            ht[h] = kvp
            kvp += self.kvlen(k) + self.kvlen(v)
            kvl.append(k)
            kvl.append(v)

        self.m = mmap(-1, kvp)
        for p in ht:
            self.m.write(uint_format.pack(p))
        for x in kvl:
            if len(x) <= 0x7f:
                self.m.write_byte(chr(len(x)))
            else:
                self.m.write(uint_format.pack(0x80000000 + len(x)))
            self.m.write(x)

    def hash(self, k):
        h = hash(k)
        h = (h + (h >> 3) + (h >> 13) + (h >> 23)) * 1749375391 & (self.hs - 1)
        return h

    def get(self, k, d=None):
        h = self.hash(k)
        while True:
            x = uint_format.unpack(self.m[h * 4:h * 4 + 4])[0]
            if x == 0xffffffff:
                return d
            self.m.seek(x)
            if k == self.read_kv():
                return self.read_kv()
            h = (h + 1) & (self.hs - 1)

    def read_kv(self):
        sz = ord(self.m.read_byte())
        if sz & 0x80:
            sz = uint_format.unpack(chr(sz) + self.m.read(3))[0] - 0x80000000
        return self.m.read(sz)

    def kvlen(self, k):
        return len(k) + (1 if len(k) <= 0x7f else 4)

    def __contains__(self, k):
        return self.get(k, None) is not None

    def close(self):
        self.m.close()

uint_format = struct.Struct('>I')


def uget(a, k, d=None):
    return to_unicode(a.get(to_str(k), d))


def uin(a, k):
    return to_str(k) in a


def to_unicode(s):
    return s.decode('utf-8') if isinstance(s, str) else s


def to_str(s):
    return s.encode('utf-8') if isinstance(s, unicode) else s


def mmap_test():
    n = 1000000
    d = shared_immutable_dict({str(i * 2): '1' for i in xrange(n)})
    start_time = default_timer()
    for i in xrange(n):
        if bool(d.get(str(i))) != (i % 2 == 0):
            raise Exception(i)
    print 'mmap speed: %d gets per sec' % (n / (default_timer() - start_time))


def manager_test():
    n = 100000
    d = Manager().dict({str(i * 2): '1' for i in xrange(n)})
    start_time = default_timer()
    for i in xrange(n):
        if bool(d.get(str(i))) != (i % 2 == 0):
            raise Exception(i)
    print 'manager speed: %d gets per sec' % (n / (default_timer() - start_time))


def shm_test():
    n = 1000000
    d = HashTable('tmp', n)
    d.update({str(i * 2): '1' for i in xrange(n)})
    start_time = default_timer()
    for i in xrange(n):
        if bool(d.get(str(i))) != (i % 2 == 0):
            raise Exception(i)
    print 'shm speed: %d gets per sec' % (n / (default_timer() - start_time))


if __name__ == '__main__':
    mmap_test()
    manager_test()
    shm_test()

I risultati delle prestazioni del mio laptop sono:

mmap speed: 247288 gets per sec
manager speed: 33792 gets per sec
shm speed: 691332 gets per sec

semplice esempio di utilizzo:

ht = shared_immutable_dict({'a': '1', 'b': '2'})
print ht.get('a')

14
Github? Documentazione? come possiamo usare questo strumento?
Pavlos Panteliadis

10

Oltre a @ senderle qui, alcuni potrebbero anche chiedersi come utilizzare la funzionalità di multiprocessing.Pool.

La cosa bella è che esiste un .Pool()metodo per l' manageristanza che imita tutte le API familiari del livello superiore multiprocessing.

from itertools import repeat
import multiprocessing as mp
import os
import pprint

def f(d: dict) -> None:
    pid = os.getpid()
    d[pid] = "Hi, I was written by process %d" % pid

if __name__ == '__main__':
    with mp.Manager() as manager:
        d = manager.dict()
        with manager.Pool() as pool:
            pool.map(f, repeat(d, 10))
        # `d` is a DictProxy object that can be converted to dict
        pprint.pprint(dict(d))

Produzione:

$ python3 mul.py 
{22562: 'Hi, I was written by process 22562',
 22563: 'Hi, I was written by process 22563',
 22564: 'Hi, I was written by process 22564',
 22565: 'Hi, I was written by process 22565',
 22566: 'Hi, I was written by process 22566',
 22567: 'Hi, I was written by process 22567',
 22568: 'Hi, I was written by process 22568',
 22569: 'Hi, I was written by process 22569',
 22570: 'Hi, I was written by process 22570',
 22571: 'Hi, I was written by process 22571'}

Questo è un esempio leggermente diverso in cui ogni processo registra semplicemente il proprio ID processo DictProxynell'oggetto globale d.


3

Forse puoi provare pyshmht , condividendo l'estensione della tabella hash basata sulla memoria per Python.

Avviso

  1. Non è completamente testato, solo per riferimento.

  2. Attualmente manca dei meccanismi lock / sem per il multiprocessing.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.