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


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 - це об'єкт спільного пошуку. Мені не потрібно це розділяти і обробляти окремо. Мені потрібно зберегти єдину його копію. Робота, яку мені потрібно розділити, - це читання багатьох інших великих файлів і пошук елементів у цих великих файлах проти об’єкта пошуку.

Подальше оновлення: база даних - це прекрасне рішення, складене схоже може бути кращим рішенням, а файл на диску (полиці або dbm) може бути ще кращим. У цьому питанні мене особливо зацікавило рішення щодо пам’яті. Для остаточного рішення я буду використовувати hadoop, але я хотів побачити, чи можу я також мати локальну версію в пам'яті.


ваш код, як написано, вимагатиме marshal.loadбатьків та кожної дитини (кожен процес імпортує модуль).
jfs

Ти маєш рацію, виправлений.
Паранд

Для "локальної пам’яті" та якщо ви хочете уникати копіювання наступного, може бути корисним 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. Запишіть кожного працівника як "фільтр" - читає проміжні результати з stdin, працює, пише проміжні результати на stdout.

  2. Підключіть усіх працівників як трубопровід:

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

Кожен процес читає, працює і пише.

Це надзвичайно ефективно, оскільки всі процеси працюють одночасно. Записи та читання проходять безпосередньо через загальні буфери між процесами.


Рішення 2

У деяких випадках у вас є більш складна структура - часто це "вентиляторна" структура. У цьому випадку у вас є батько з кількома дітьми.

  1. Батько відкриває вихідні дані. Батьківські вилки мають кілька дітей.

  2. Батько читає джерело, обробляє частини джерела для кожної одночасно працюючої дитини.

  3. Коли батько досягне кінця, закрийте трубу. Дитина отримує кінець файлу і закінчується нормально.

Дитячі частини приємно писати, тому що кожна дитина просто читає sys.stdin.

Батько має трохи фантазійних кроків у нерестування всіх дітей та правильному утриманні труб, але це не дуже погано.

Вентилятор - це протилежна структура. Ряд незалежно запущених процесів потребує переплетення своїх входів у загальний процес. Колекціонер не так просто писати, оскільки його доводиться читати з багатьох джерел.

Зчитування з багатьох названих труб часто проводиться за допомогою selectмодуля, щоб побачити, які труби мають вхідні дані.


Рішення 3

Спільний пошук - це визначення бази даних.

Рішення 3A - завантаження бази даних. Нехай працівники обробляють дані в базі даних.

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


Рішення 4

Об'єкт файлової системи. Unix OS пропонує об'єкти спільної пам'яті. Це лише файли, які відображаються в пам'яті, так що заміна вводу-виводу робиться замість більш чистих буферних зчитувань.

Це можна зробити з контексту Python кількома способами

  1. Напишіть програму запуску, яка (1) розбиває ваш оригінальний гігантський об'єкт на більш дрібні об'єкти, і (2) запускає працівників, кожен з яких має менший об'єкт. Дрібні об'єкти можуть бути мариновані об'єктами Python, щоб заощадити невеликий час для читання файлів.

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

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


Мої процеси насправді не підходять; вони однакові, просто обробляють різні фрагменти даних.
Паранд

Вони часто можуть бути структуровані як фільтри. Вони читають свої дані, виконують свою роботу та записують свій результат для подальшої обробки.
С.Лотт

Мені подобається ваше рішення, але що відбувається з блокуванням вводу / виводу? Що робити, якщо батько блокує читання / письмо від / до одного з своїх дітей? Вибір повідомляє вас про те, що ви можете писати, але не говорить про те, скільки. Те саме для читання.
Крістіан Цюпіту

Це окремі процеси - батьки та діти не заважають один одному. Кожен байт, що утворюється на одному кінці труби, одразу стає доступним на іншому кінці для споживання - труба є спільним буфером. Не впевнений, що означає ваше запитання в цьому контексті.
С.Лотт

Я можу перевірити, що сказав С.Лотт. Мені були потрібні ті самі операції, що були зроблені над одним файлом. Таким чином, перший працівник виконував свою функцію в кожному рядку з номером% 2 == 0 і зберігав його у файл, а інші рядки відправляв до наступного процесу трубопроводу (який був тим самим сценарієм). Час виконання знизився вдвічі. Це трохи хакі, але накладні витрати набагато легші, ніж карта / пуп у багатопроцесорному модулі.
Вінс

36

Чи породжуються дочірні процеси за допомогою багатопроцесорних об'єктів спільного використання, створених раніше в програмі?

Це залежить. Для глобальних змінних лише для читання це часто можна вважати (окрім споживаної пам'яті), інакше не слід.

Багатопроцесорна документація говорить:

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

6
Так? Як поділитися z через процеси?
cbare

4
@cbare: Добре запитання! z насправді не є спільним, як показує вихід зі сном. Вихід без сну показує, що один процес обробляє (PID = 1148) всю роботу; що ми бачимо в останньому прикладі - це значення z для цього єдиного процесу.
Ерік О Лебігот

Ця відповідь показує, що zїї не поділяють. Таким чином, це дає відповідь на запитання: "ні, принаймні в Windows батьківська змінна не ділиться між дітьми".
Ерік О Лебігот

@EOL: технічно ви правильні, але на практиці, якщо дані читаються лише (на відміну від zвипадків), вони можуть вважатися спільними.
jfs

Для уточнення, твердження Майте на увазі, що якщо запуск коду в дочірньому процесі намагається отримати доступ до глобальної змінної ... в 2.7 документах йдеться про Python, який працює під Windows.
користувач1071847

28

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

У більшості * nix систем, використовуючи виклик нижчого рівня до os.fork() фактично надасть вам пам'ять під час запису, яка може бути те, що ви думаєте. AFAIK, теоретично, в найбільш спрощених програмах, які ви можете прочитати, ви можете читати з цих даних, не дублюючи їх.

Однак в інтерпретаторі Python все не так просто. Дані об'єкта та метадані зберігаються в одному сегменті пам’яті, тому навіть якщо об’єкт ніколи не змінюється, щось на зразок лічильника посилань на цей об’єкт збільшується, викликає запис у пам'ять, а отже, і копію. Практично будь-яка програма Python, яка робить більше, ніж "друк" привіт ", призведе до збільшення кількості посилань, тому ви, ймовірно, ніколи не зрозумієте перевагу копіювання під час запису.

Навіть якби комусь вдалося зламати рішення спільної пам’яті в Python, намагання координувати збирання сміття в різних процесах, ймовірно, буде досить болісно.


3
У цьому випадку буде скопійовано лише область пам’яті, що позначає кількість посилань, не обов'язково великі дані, доступні лише для читання, чи не так?
kawing-chiu

7

Якщо ви працюєте під Unix, вони можуть спільно використовувати один і той же об’єкт, завдяки тому, як працює вилка (тобто, дочірні процеси мають окрему пам'ять, але це копія при записі, тому вона може бути спільною, доки ніхто її не змінює). Я спробував таке:

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
А що з смітником? Що відбувається, коли він працює? Чи не змінюється макет пам'яті?
Крістіан Цюпіту

Це обгрунтоване занепокоєння. Чи вплине це на Паранда, залежатиме від того, наскільки він все це використовує та наскільки надійним він повинен бути цей код. Якщо він не працював для нього, я б рекомендував використовувати модуль mmap для більшого контролю (припускаючи, що він хоче дотримуватися цього базового підходу).
Джейкоб Габріельсон

Я опублікував оновлений ваш приклад: stackoverflow.com/questions/659865 / ...
JFS

@JacobGabrielson: копія зроблена. Оригінальне питання стосується того, чи зроблена копія.
abhinavkulkarni

3

Різні процеси мають різний адресний простір. Як і запуск різних примірників перекладача. Ось для чого призначений IPC (міжпроцесовий зв'язок).

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

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


2
Я не думаю, що IPC було б доречним для цього; це дані лише для читання, до яких потрібен кожен. Немає сенсу передавати його між процесами; в гіршому випадку кожен може прочитати свою копію. Я намагаюся зберегти пам'ять, не маючи окремої копії в кожному процесі.
Паранд

Ви можете мати головний процес, делегуючи фрагменти даних для роботи іншим підлеглим процесам. Або раби можуть запитувати дані, або вони можуть підштовхувати дані. Таким чином, не кожен процес матиме копію всього об’єкта.
Василь

1
@Vasil: Що робити, якщо для кожного процесу потрібен весь набір даних, і він просто виконує іншу операцію на ньому?
Буде

1

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


Хороший момент, я безпосередньо не порівнював продуктивність диска та пам'яті. Я припускав, що буде велика різниця, але насправді я не перевіряв.
Паранд

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)    

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.