Как я могу ограничить объем процесса многопроцессорности?

Используя python multiprocessing, следующий надуманный пример работает с минимальными требованиями к памяти:

import multiprocessing 
# completely_unrelated_array = range(2**25)

def foo(x):
    for x in xrange(2**28):pass
    print x**2

P = multiprocessing.Pool()

for x in range(8):
    multiprocessing.Process(target=foo, args=(x,)).start()

Раскомментируйте создание completely_unrelated_array, и вы обнаружите, что каждый порожденный процесс выделяет память для копии completely_unrelated_array! Это минимальный пример гораздо более крупного проекта, который я не могу понять, как обходиться; многопроцессорность, похоже, делает копию всего глобального. Мне не нужен объект общей памяти, мне просто нужно передать x и обработать его без служебных программ памяти всей программы.

Боковое наблюдение: интересно то, что print id(completely_unrelated_array) внутри foo дает одно и то же значение, предполагая, что каким-то образом это могут быть не копии...

Ответ 1

Из-за природы os.fork() любые переменные в глобальном пространстве имен вашего модуля __main__ будут наследоваться дочерними процессами (при условии, что вы находитесь на платформе Posix), поэтому вы увидите использование памяти в детях отражают это, как только они созданы. Я не уверен, что вся эта память действительно выделяется, насколько я знаю, что память разделяется до тех пор, пока вы на самом деле не попытаетесь изменить ее в ребёнке, после чего будет создана новая копия. Windows, с другой стороны, не использует os.fork() - он повторно импортирует основной модуль в каждый дочерний элемент и рассосывает любые локальные переменные, которые вы хотите отправить детям. Таким образом, используя Windows, вы можете фактически избежать большого глобального копирования, скопированного в дочерний элемент, только определяя его внутри защитника if __name__ == "__main__":, потому что все внутри этого защитника будет выполняться только в родительском процессе:

import time
import multiprocessing 


def foo(x):
    for x in range(2**28):pass
    print(x**2)

if __name__ == "__main__":
    completely_unrelated_array = list(range(2**25)) # This will only be defined in the parent on Windows
    P = multiprocessing.Pool()

    for x in range(8):
        multiprocessing.Process(target=foo, args=(x,)).start()

Теперь, в Python 2.x, вы можете создавать только новые объекты multiprocessing.Process путем форкирования, если вы используете платформу Posix. Но на Python 3.4 вы можете указать, как создавать новые процессы, используя контексты. Итак, мы можем указать "spawn" контекст, который используется Windows, для создания наших новых процессов и использования того же трюка:

# Note that this is Python 3.4+ only
import time
import multiprocessing 

def foo(x):
    for x in range(2**28):pass
    print(x**2)


if __name__ == "__main__":
    completely_unrelated_array = list(range(2**23))  # Again, this only exists in the parent
    ctx = multiprocessing.get_context("spawn") # Use process spawning instead of fork
    P = ctx.Pool()

    for x in range(8):
        ctx.Process(target=foo, args=(x,)).start()

Если вам нужна поддержка 2.x или вы хотите использовать os.fork() для создания новых объектов Process, я думаю, что лучшее, что вы можете сделать, чтобы отключить использование записанной памяти, - это немедленно удалить оскорбительный объект в ребенок:

import time
import multiprocessing 
import gc

def foo(x):
    init()
    for x in range(2**28):pass
    print(x**2)

def init():
    global completely_unrelated_array
    completely_unrelated_array = None
    del completely_unrelated_array
    gc.collect()

if __name__ == "__main__":
    completely_unrelated_array = list(range(2**23))
    P = multiprocessing.Pool(initializer=init)

    for x in range(8):
        multiprocessing.Process(target=foo, args=(x,)).start()
    time.sleep(100)

Ответ 2

Важно то, какую платформу вы нацеливаете. Процессы Unix-систем создаются с использованием памяти Copy-On-Write (корова). Поэтому, несмотря на то, что каждый процесс получает копию полной памяти родительского процесса, эта память фактически распределяется только на основе каждой страницы (4KiB), когда она модифицируется. Поэтому, если вы используете только таргетинг на эти платформы, вам ничего не нужно менять.

Если вы нацеливаете платформы без вилок, вы можете использовать python 3.4 и новые контексты forking spawn и forkserver, см. documentation Эти методы будут создавать новые процессы, которые не имеют ничего общего или имеют ограниченное состояние с родителем, и вся передача данных явно.

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

import multiprocessing  as mp
import numpy as np

def foo(x):
    import time
    time.sleep(60)

if __name__ == "__main__":
    mp.set_start_method('spawn')
    # not global so forks will not have this allocated due to the spawn method
    # if the method would be fork the children would still have this memory allocated
    # but it could be copy-on-write
    completely_unrelated_array = np.ones((5000, 10000))
    P = mp.Pool()
    for x in range(3):
        mp.Process(target=foo, args=(x,)).start()

например, верхний вывод с помощью spawn:

%MEM     TIME+ COMMAND
29.2   0:00.52 python3                                                
0.5   0:00.00 python3    
0.5   0:00.00 python3    
0.5   0:00.00 python3    

и с fork:

%MEM     TIME+ COMMAND
29.2   0:00.52 python3                                                
29.1   0:00.00 python3    
29.1   0:00.00 python3                                                
29.1   0:00.00 python3

обратите внимание, как его более 100% из-за копирования-на-записи