Многопроцессорность - совместное использование сложного объекта

У меня есть большой объект dict, который нужно разделить между несколькими рабочими процессами. Каждый работник считывает случайное подмножество информации в объекте и выполняет некоторые вычисления с ним. Я бы хотел избежать копирования большого объекта, так как моя машина быстро исчерпала память.

Я играл с кодом для этого вопроса SO и немного изменил его, чтобы использовать пул процессов фиксированного размера, который лучше подходит для моего использования, Однако это, похоже, нарушает его.

from multiprocessing import Process, Pool
from multiprocessing.managers import BaseManager

class numeri(object):
    def __init__(self):
        self.nl = []

    def getLen(self):
        return len(self.nl)

    def stampa(self):
        print self.nl

    def appendi(self, x):
        self.nl.append(x)

    def svuota(self):
        for i in range(len(self.nl)):
            del self.nl[0]

class numManager(BaseManager):
    pass

def produce(listaNumeri):
    print 'producing', id(listaNumeri)
    return id(listaNumeri)

def main():
    numManager.register('numeri', numeri, exposed=['getLen', 'appendi',
                        'svuota', 'stampa'])
    mymanager = numManager()
    mymanager.start()
    listaNumeri = mymanager.numeri()
    print id(listaNumeri)

    print '------------ Process'
    for i in range(5):
        producer = Process(target=produce, args=(listaNumeri,))
        producer.start()
        producer.join()

    print '--------------- Pool'
    pool = Pool(processes=1)
    for i in range(5):
        pool.apply_async(produce, args=(listaNumeri,)).get()

if __name__ == '__main__':
    main()

Выходной сигнал

4315705168
------------ Process
producing 4315705168
producing 4315705168
producing 4315705168
producing 4315705168
producing 4315705168
--------------- Pool
producing 4299771152
producing 4315861712
producing 4299771152
producing 4315861712
producing 4299771152

Как вы можете видеть, в первом случае все рабочие процессы получают один и тот же объект (по id). Во втором случае идентификатор не совпадает. Означает ли это, что объект копируется?

P.S. Я не думаю, что это важно, но я использую joblib, который внутренне использовал Pool:

from joblib import delayed, Parallel

print '------------- Joblib'
        Parallel(n_jobs=4)(delayed(produce)(listaNumeri) for i in range(5))

который выводит:

------------- Joblib
producing 4315862096
producing 4315862288
producing 4315862480
producing 4315862672
producing 4315862352

Ответ 1

Я боюсь, что практически ничего здесь не работает так, как вы надеетесь, что это работает: - (

Вначале отметим, что идентичные значения id(), создаваемые различными процессами, ничего не говорят о том, действительно ли объекты являются одним и тем же объектом. Каждый процесс имеет собственное виртуальное адресное пространство, назначенное операционной системой. Один и тот же виртуальный адрес в двух процессах может относиться к совершенно другим физическим ячейкам памяти. Независимо от того, производит ли ваш код один и тот же вывод id() или нет, это в значительной степени чисто случайный. Во время нескольких прогонов иногда я вижу другой id() вывод в разделе Process и повторный вывод id() в вашем разделе Pool или наоборот, или и тот и другой.

Во-вторых, Manager предоставляет семантический общий доступ, но не физический. Данные для вашего экземпляра numeri живут только в процессе менеджера. Все ваши рабочие процессы видят (копии) прокси-объектов. Это тонкие обертки, которые направляют все операции, которые должны выполняться процессом менеджера. Это связано с большим количеством взаимодействия между процессами и сериализации внутри процесса менеджера. Это отличный способ написать очень медленный код;-) Да, есть только одна копия данных numeri, но все работы над ним выполняются одним процессом (процессом менеджера).

Чтобы увидеть это более четко, внесите изменения, предложенные @martineau, а также измените get_list_id() на это:

def get_list_id(self):  # added method
    import os
    print("get_list_id() running in process", os.getpid())
    return id(self.nl)

Здесь вывод образца:

41543664
------------ Process
producing 42262032
get_list_id() running in process 5856
with list_id 44544608
producing 46268496
get_list_id() running in process 5856
with list_id 44544608
producing 42262032
get_list_id() running in process 5856
with list_id 44544608
producing 44153904
get_list_id() running in process 5856
with list_id 44544608
producing 42262032
get_list_id() running in process 5856
with list_id 44544608
--------------- Pool
producing 41639248
get_list_id() running in process 5856
with list_id 44544608
producing 41777200
get_list_id() running in process 5856
with list_id 44544608
producing 41776816
get_list_id() running in process 5856
with list_id 44544608
producing 41777168
get_list_id() running in process 5856
with list_id 44544608
producing 41777136
get_list_id() running in process 5856
with list_id 44544608

Clear? Причина, по которой вы получаете одинаковый идентификатор списка каждый раз не, потому что каждый рабочий процесс имеет тот же член self.nl, потому что все numeri методы выполняются в один процесс (процесс менеджера). Поэтому идентификатор списка всегда один и тот же.

Если вы работаете в системе Linux-y (ОС, поддерживающей fork()), гораздо лучше забыть все эти вещи Manager и создать сложный объект на уровне модуля, прежде чем запускать какие-либо рабочие процессы, Затем рабочие наследуют (адресные копии) вашего сложного объекта. Обычная семантика copy-on-write fork() сделает это максимально эффективным с точки зрения памяти. Это достаточно, если мутации не нужно сбрасывать обратно в основную программную копию сложного объекта. Если мутации нужно сбросить назад, то вы снова нуждаетесь в большом количестве межпроцессного общения, а multiprocessing становится менее привлекательным.

Здесь нет простых ответов. Не стреляйте в посланника; -)

Ответ 2

Если вы добавите две строки в свой код, вы найдете что-то очень странное в этом поведении:

def produce(listaNumeri):
    print 'producing', id(listaNumeri)
    print listaNumeri # <- New line
    return id(listaNumeri)


def main():
    numManager.register('numeri', numeri, exposed=['getLen', 'appendi', 'svuota', 'stampa', 'getAll'])
    mymanager = numManager()
    mymanager.start()
    listaNumeri = mymanager.numeri()
    print listaNumeri # <- New line
    print id(listaNumeri)

Это дает вам следующий результат:

<__main__.numeri object at 0x103892990>
4354247888
------------ Process
producing 4354247888
<__main__.numeri object at 0x103892990>
producing 4354247888
<__main__.numeri object at 0x103892990>
producing 4354247888
<__main__.numeri object at 0x103892990>
producing 4354247888
<__main__.numeri object at 0x103892990>
producing 4354247888
<__main__.numeri object at 0x103892990>
--------------- Pool
producing 4352988560
<__main__.numeri object at 0x103892990>
producing 4354547664
<__main__.numeri object at 0x103892990>
producing 4352988560
<__main__.numeri object at 0x103892990>
producing 4354547664
<__main__.numeri object at 0x103892990>
producing 4352988560
<__main__.numeri object at 0x103892990>

Как вы можете видеть, каждый объект один и тот же, но идентификатор не всегда один и тот же. Кроме того, посмотрите на идентификаторы, используемые во время секции пула - он переключается между двумя идентификаторами.

Ответ на то, что происходит, происходит от фактической печати атрибута __class__ во время produce. Каждый прогон, __class__ на самом деле

<class 'multiprocessing.managers.AutoProxy[numeri]'>

Таким образом, объект numeri каждый раз обертывается AutoProxy, а AutoProxy не всегда одинаковый. Однако объект numeri, который обертывается, одинаковый при каждом вызове produce. Если вы вызовете метод appendi один раз в produce, тогда listaNumeri закончит с 10 элементами в конце вашей программы.

Ответ 3

Вы путаете экземпляр объекта numeri с его менеджером listaNumeri. Это можно проиллюстрировать, сделав несколько небольших модификаций кода:

Сначала добавьте метод get_list_id к class numeri(object), который возвращает id фактической внутренней структуры данных:

    ...                                                   
    def get_list_id(self):  # added method
        return id(self.nl)

Затем измените produce(), чтобы использовать его:

def produce(listaNumeri):
    print 'producing', id(listaNumeri)
    print ' with list_id', listaNumeri.get_list_id()  # added
    return id(listaNumeri)

Наконец, не забудьте открыть новый метод как часть интерфейса numManager:

def main():
    numManager.register('numeri', numeri, exposed=['getLen', 'appendi',
                                                   'svuota', 'stampa',
                                                   'get_list_id'])  # added
    ...                                                   

Впоследствии вы увидите что-то вроде следующего вывода:

13195568
------------ Process
producing 12739600
 with list_id 13607080
producing 12739600
 with list_id 13607080
producing 12739600
 with list_id 13607080
producing 12739600
 with list_id 13607080
producing 12739600
 with list_id 13607080
--------------- Pool
producing 13690384
 with list_id 13607080
producing 13691920
 with list_id 13607080
producing 13691888
 with list_id 13607080
producing 13691856
 with list_id 13607080
producing 13691824
 with list_id 13607080

Как видно из этого, несмотря на то, что для каждого процесса Pool существует другой объект Manager, все они используют (разделяют) один и тот же "управляемый" объект данных.