многопроцессорность: совместное использование большого объекта только для чтения между процессами?

107

Дочерние процессы порождены через многопроцессорные общие объекты, созданные ранее в программе?

У меня следующая установка:

do_some_processing(filename):
    for line in file(filename):
        if line.split(',')[0] in big_lookup_object:
            # something here

if __name__ == '__main__':
    big_lookup_object = marshal.load('file.bin')
    pool = Pool(processes=4)
    print pool.map(do_some_processing, glob.glob('*.data'))

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

Мой вопрос: загружается ли большой объект в общую память, как если бы я создал процесс в unix / c, или каждый процесс загружает свою собственную копию большого объекта?

Обновление: чтобы уточнить - big_lookup_object - это общий поисковый объект. Мне не нужно разбивать это на части и обрабатывать отдельно. Мне нужно сохранить единственную его копию. Работа, которую мне нужно разделить, - это чтение множества других больших файлов и поиск элементов в этих больших файлах по объекту поиска.

Дальнейшее обновление: база данных - прекрасное решение, memcached может быть лучшим решением, а файл на диске (полка или dbm) может быть еще лучше. В этом вопросе меня особенно интересовало решение в памяти. В качестве окончательного решения я буду использовать hadoop, но я хотел посмотреть, могу ли я также иметь локальную версию в памяти.

Parand
источник
ваш код, как написано, будет вызывать marshal.loadродительский и для каждого дочернего (каждый процесс импортирует модуль).
jfs
Вы правы, поправили.
Parand
Для «локального в памяти» и если вы не хотите копировать, может быть полезно следующее: docs.python.org/library/…
jfs
доля нет. порожденные процессы (например, fork или exec) являются точной копией вызывающего процесса ... но в другой памяти. Чтобы один процесс мог взаимодействовать с другим, вам потребуется межпроцессное взаимодействие или чтение / запись IPC в некоторую общую область памяти.
ron

Ответы:

50

"Дочерние процессы порождены через многопроцессорные общие объекты, созданные ранее в программе?"

Нет (python до 3.8) и Да в 3.8 ( https://docs.python.org/3/library/multiprocessing.shared_memory.html#module-multiprocessing.shared_memory )

Процессы имеют независимое пространство памяти.

Решение 1

Сделайте это, чтобы наилучшим образом использовать большую структуру с большим количеством рабочих.

  1. Запишите каждого рабочего как «фильтр» - считывает промежуточные результаты из стандартного ввода, выполняет работу, записывает промежуточные результаты в стандартный вывод.

  2. Подключите всех воркеров в конвейер:

    process1 <source | process2 | process3 | ... | processn >result

Каждый процесс читает, работает и пишет.

Это замечательно эффективно, поскольку все процессы выполняются одновременно. Записи и чтения проходят напрямую через общие буферы между процессами.


Решение 2

В некоторых случаях у вас более сложная структура - часто «разветвленная» структура. В этом случае у вас есть родитель с несколькими детьми.

  1. Родитель открывает исходные данные. Родитель разветвляет несколько детей.

  2. Родитель читает исходный код, передает части источника каждому параллельно запущенному дочернему элементу.

  3. Когда родитель дойдет до конца, закройте трубу. Ребенок получает конец файла и заканчивает нормально.

Дочерние части приятно писать, потому что каждый ребенок просто читает sys.stdin.

У родителя есть немного причудливой работы по созданию всех потомков и правильному удержанию труб, но это не так уж плохо.

Фан-ин - это противоположная структура. Ряду независимо выполняющихся процессов необходимо чередовать свои входные данные в общий процесс. Коллектор не так просто написать, так как он должен читать из множества источников.

Чтение из многих именованных каналов часто выполняется с помощью selectмодуля, чтобы увидеть, какие каналы имеют ожидающий ввод.


Решение 3

Общий поиск - это определение базы данных.

Решение 3А - загрузить базу данных. Позвольте рабочим обработать данные в базе данных.

Решение 3B - создайте очень простой сервер, используя werkzeug (или аналогичный), чтобы предоставить приложениям WSGI, которые отвечают на HTTP GET, чтобы рабочие могли запрашивать сервер.


Решение 4

Объект общей файловой системы. ОС Unix предлагает объекты разделяемой памяти. Это просто файлы, которые отображаются в памяти, так что ввод-вывод подкачки выполняется вместо операций чтения с буферизацией по соглашению.

Вы можете сделать это из контекста Python несколькими способами.

  1. Напишите программу запуска, которая (1) разбивает исходный гигантский объект на более мелкие объекты и (2) запускает рабочие процессы, каждый с меньшим объектом. Меньшие объекты могут быть обработаны объектами Python, чтобы сэкономить немного времени на чтение файла.

  2. Напишите программу запуска, которая (1) считывает ваш исходный гигантский объект и записывает файл со структурой страниц и байтовым кодом, используя seekоперации, чтобы гарантировать, что отдельные разделы легко найти с помощью простых поисков. Это то, что делает ядро ​​базы данных - разбивает данные на страницы, упрощает поиск каждой страницы с помощью файла seek.

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

С.Лотт
источник
Мои процессы на самом деле не подходят; они все одинаковы, просто обрабатывают разные фрагменты данных.
Parand
Часто их можно структурировать как фильтры. Они читают свои данные, выполняют свою работу и записывают результат для последующей обработки.
S.Lott
Мне нравится ваше решение, но что происходит с блокировкой ввода-вывода? Что, если родитель блокирует чтение / запись от / до одного из своих дочерних элементов? Select уведомляет вас о том, что вы можете писать, но не говорит, сколько. То же и для чтения.
Cristian Ciupitu,
Это отдельные процессы - родители и дети не мешают друг другу. Каждый байт, созданный на одном конце канала, сразу же доступен на другом конце для использования - канал является общим буфером. Не уверен, что означает ваш вопрос в этом контексте.
S.Lott
Я могу проверить, что сказал С.Лотт. Мне нужны были те же операции, что и с одним файлом. Итак, первый рабочий запускал свою функцию в каждой строке с номером% 2 == 0 и сохранял ее в файл, а остальные строки отправлял следующему конвейерному процессу (который был тем же сценарием). Время работы сократилось вдвое. Это немного взломано, но накладные расходы намного меньше, чем у map / poop в многопроцессорном модуле.
Винс
36

Дочерние процессы порождены через многопроцессорные общие объекты, созданные ранее в программе?

Это зависит. Для глобальных переменных, доступных только для чтения, это часто можно считать так (помимо потребляемой памяти), иначе этого не должно быть.

документация multiprocessing говорит:

Better to inherit than pickle/unpickle

В Windows многие типы из многопроцессорной обработки должны иметь возможность выбора, чтобы дочерние процессы могли их использовать. Однако обычно следует избегать отправки общих объектов другим процессам с использованием каналов или очередей. Вместо этого вы должны организовать программу так, чтобы процесс, которому требуется доступ к совместно используемому ресурсу, созданному где-то еще, мог унаследовать его от процесса-предка.

Explicitly pass resources to child processes

В Unix дочерний процесс может использовать общий ресурс, созданный в родительском процессе с использованием глобального ресурса. Однако лучше передать объект в качестве аргумента конструктору дочернего процесса.

Помимо обеспечения (потенциально) совместимости кода с Windows, это также гарантирует, что пока дочерний процесс все еще жив, объект не будет собираться сборщиком мусора в родительском процессе. Это может быть важно, если какой-то ресурс освобождается при сборке мусора в родительском процессе.

Global variables

Имейте в виду, что если код, выполняемый в дочернем процессе, пытается получить доступ к глобальной переменной, тогда значение, которое он видит (если есть), может не совпадать со значением в родительском процессе во время вызова Process.start () .

пример

В Windows (один процессор):

#!/usr/bin/env python
import os, sys, time
from multiprocessing import Pool

x = 23000 # replace `23` due to small integers share representation
z = []    # integers are immutable, let's try mutable object

def printx(y):
    global x
    if y == 3:
       x = -x
    z.append(y)
    print os.getpid(), x, id(x), z, id(z) 
    print y
    if len(sys.argv) == 2 and sys.argv[1] == "sleep":
       time.sleep(.1) # should make more apparant the effect

if __name__ == '__main__':
    pool = Pool(processes=4)
    pool.map(printx, (1,2,3,4))

С sleep:

$ python26 test_share.py sleep
2504 23000 11639492 [1] 10774408
1
2564 23000 11639492 [2] 10774408
2
2504 -23000 11639384 [1, 3] 10774408
3
4084 23000 11639492 [4] 10774408
4

Без sleep:

$ python26 test_share.py
1148 23000 11639492 [1] 10774408
1
1148 23000 11639492 [1, 2] 10774408
2
1148 -23000 11639324 [1, 2, 3] 10774408
3
1148 -23000 11639324 [1, 2, 3, 4] 10774408
4
jfs
источник
6
А? Как z распределяется между процессами ??
cbare
4
@cbare: Хороший вопрос! На самом деле z не используется совместно, как показывает вывод со сном. Вывод без сна показывает, что всю работу выполняет один процесс (PID = 1148); в последнем примере мы видим значение z для этого единственного процесса.
Эрик О Лебигот
Этот ответ показывает, что zникто не поделился. Таким образом, это отвечает на вопрос: «Нет, по крайней мере, в Windows родительская переменная не используется совместно между дочерними элементами».
Eric O Lebigot 01
@EOL: технически вы правы, но на практике, если данные доступны только для чтения (в отличие от zслучая), они могут считаться общими.
jfs
Чтобы прояснить это утверждение, имейте в виду, что если код, выполняемый в дочернем процессе, пытается получить доступ к глобальной переменной ... в документах 2.7 имеется в виду Python, работающий под Windows.
user1071847
28

С.Лотт прав. Комбинации клавиш для многопроцессорной обработки в Python фактически предоставляют вам отдельный дублированный фрагмент памяти.

В большинстве систем * nix использование вызова нижнего уровня на os.fork()самом деле даст вам память для копирования при записи, о чем вы можете подумать. AFAIK, теоретически, в самой упрощенной из возможных программ, вы могли бы читать из этих данных, не дублируя их.

Однако в интерпретаторе Python все не так просто. Данные объекта и метаданные хранятся в одном и том же сегменте памяти, поэтому даже если объект никогда не изменяется, что-то вроде счетчика ссылок для этого объекта, который увеличивается, вызовет запись в память и, следовательно, копию. Практически любая программа Python, которая делает больше, чем «print 'hello», вызовет приращение счетчика ссылок, так что вы, вероятно, никогда не поймете преимущества копирования при записи.

Даже если бы кому-то удалось взломать решение с общей памятью в Python, попытка координировать сбор мусора между процессами, вероятно, была бы довольно болезненной.

Джаррет Харди
источник
3
В этом случае будет скопирована только область памяти счетчика ссылок, не обязательно большие данные, доступные только для чтения, не так ли?
kawing-chiu
7

Если вы работаете под Unix, они могут совместно использовать один и тот же объект из-за того, как работает fork (т. Е. У дочерних процессов есть отдельная память, но она копируется при записи, поэтому она может совместно использоваться, пока ее никто не изменяет). Я пробовал следующее:

import multiprocessing

x = 23

def printx(y):
    print x, id(x)
    print y

if __name__ == '__main__':
    pool = multiprocessing.Pool(processes=4)
    pool.map(printx, (1,2,3,4))

и получил следующий результат:

$ ./mtest.py
23 22995656
1
23 22995656
2
23 22995656
3
23 22995656
4

Конечно, это не доказывает, что копия не была сделана, но вы должны иметь возможность проверить это в своей ситуации, посмотрев на вывод, psчтобы увидеть, сколько реальной памяти использует каждый подпроцесс.

Джейкоб Габриэльсон
источник
2
А как насчет сборщика мусора? Что происходит при запуске? Разве не меняется раскладка памяти?
Cristian Ciupitu,
Это серьезное беспокойство. Повлияет ли это на Паранда, будет зависеть от того, как он все это использует и насколько надежным должен быть этот код. Если бы у него это не сработало, я бы рекомендовал использовать модуль mmap для большего контроля (при условии, что он хочет придерживаться этого базового подхода).
Джейкоб Габриэльсон,
Я опубликовал обновление для вашего примера: stackoverflow.com/questions/659865/…
jfs
@JacobGabrielson: Копия сделана. Первоначальный вопрос заключается в том, сделана ли копия.
abhinavkulkarni
3

У разных процессов разное адресное пространство. Как запускать разные экземпляры интерпретатора. Вот для чего нужен IPC (межпроцессное взаимодействие).

Для этого можно использовать очереди или каналы. Вы также можете использовать rpc поверх tcp, если хотите распределить процессы по сети позже.

http://docs.python.org/dev/library/multiprocessing.html#exchanging-objects-between-processes

Василь
источник
2
Я не думаю, что IPC подойдет для этого; это данные только для чтения, к которым нужен доступ каждому. Нет смысла передавать его между процессами; в худшем случае каждый может прочитать свой экземпляр. Я пытаюсь сэкономить память, не имея отдельной копии в каждом процессе.
Parand
У вас может быть главный процесс, делегирующий часть данных для работы другим подчиненным процессам. Либо ведомые устройства могут запрашивать данные, либо они могут передавать данные. Таким образом, не каждый процесс будет иметь копию всего объекта.
Vasil
1
@Vasil: Что, если каждому процессу нужен весь набор данных, и он просто выполняет с ним разные операции?
Will
1

Не имеет прямого отношения к многопроцессорности как таковой, но из вашего примера может показаться, что вы можете просто использовать модуль полки или что-то в этом роде. Неужели "big_lookup_object" действительно должен быть полностью в памяти?


источник
Хороший момент, я не сравнивал напрямую производительность на диске и в памяти. Я предполагал, что будет большая разница, но на самом деле не тестировал.
Parand
1

Нет, но вы можете загрузить свои данные как дочерний процесс и разрешить ему делиться своими данными с другими дочерними процессами. увидеть ниже.

import time
import multiprocessing

def load_data( queue_load, n_processes )

    ... load data here into some_variable

    """
    Store multiple copies of the data into
    the data queue. There needs to be enough
    copies available for each process to access. 
    """

    for i in range(n_processes):
        queue_load.put(some_variable)


def work_with_data( queue_data, queue_load ):

    # Wait for load_data() to complete
    while queue_load.empty():
        time.sleep(1)

    some_variable = queue_load.get()

    """
    ! Tuples can also be used here
    if you have multiple data files
    you wish to keep seperate.  
    a,b = queue_load.get()
    """

    ... do some stuff, resulting in new_data

    # store it in the queue
    queue_data.put(new_data)


def start_multiprocess():

    n_processes = 5

    processes = []
    stored_data = []

    # Create two Queues
    queue_load = multiprocessing.Queue()
    queue_data = multiprocessing.Queue()

    for i in range(n_processes):

        if i == 0:
            # Your big data file will be loaded here...
            p = multiprocessing.Process(target = load_data,
            args=(queue_load, n_processes))

            processes.append(p)
            p.start()   

        # ... and then it will be used here with each process
        p = multiprocessing.Process(target = work_with_data,
        args=(queue_data, queue_load))

        processes.append(p)
        p.start()

    for i in range(n_processes)
        new_data = queue_data.get()
        stored_data.append(new_data)    

    for p in processes:
        p.join()
    print(processes)    
Мотт Кортеж
источник
-4

Для платформы Linux / Unix / MacOS forkmap - быстрое и грязное решение.

Максим Имакаев
источник