Ограничение количества процессов, запущенных одновременно из скрипта Python

Я запускаю сценарий резервного копирования, который запускает дочерние процессы для выполнения резервного копирования с помощью rsync. Однако у меня нет возможности ограничить количество rsync, запускаемых одновременно.

Вот код, над которым я работаю в данный момент:

print "active_children: ", multiprocessing.active_children()
print "active_children len: ", len(multiprocessing.active_children())
while len(multiprocessing.active_children()) > 49:
   sleep(2)
p = multiprocessing.Process(target=do_backup, args=(shash["NAME"],ip,shash["buTYPE"], ))
jobs.append(p)
p.start()

Это показывает максимум одного ребенка, когда у меня запущены сотни rsync. Вот код, который фактически запускает rsync (изнутри функции do_backup), где command является переменной, содержащей строку rsync:

print command
subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
return 1

Если я добавлю sleep(x) к функции do_backup, она будет отображаться как активный дочерний элемент, пока он спит. Кроме того, таблица процессов показывает, что процессы rsync имеют PPID, равный 1. Из этого я предполагаю, что rsync отделяется и больше не является дочерним элементом python, что позволяет моему дочернему процессу умереть, поэтому я больше не могу его считать . Кто-нибудь знает, как сохранить дочерний элемент python живым и учитывать его до завершения rsync?


comment
Рассматривали ли вы возможность использования пула процессов?   -  person moooeeeep    schedule 28.10.2014


Ответы (3)


Давайте сначала развеем некоторые заблуждения

Из этого я предполагаю, что rsync отделяется и больше не является дочерним элементом python, что позволяет моему дочернему процессу умереть, поэтому я больше не могу его сосчитать.

rsync "отделяется". В системах UNIX это называется форком.

Когда процесс разветвляется, создается дочерний процесс, поэтому rsync является дочерним элементом python. Этот дочерний процесс выполняется независимо от родителя — и одновременно («одновременно»).

Процесс может управлять своими дочерними элементами. Для этого существуют специальные системные вызовы, но это немного не по теме, когда речь идет о python, который собственные высокоуровневые интерфейсы

Если вы посмотрите документацию subprocess.Popen, вы заметите, что это не вызов функции вообще: это класс. Вызвав его, вы создадите экземпляр этого класса — объект Popen. . Такие объекты имеют несколько методов. В частности, wait позволит вам блокировать ваш родительский процесс (python) до завершения дочернего процесса.


Имея это в виду, давайте взглянем на ваш код и немного упростим его:

p = multiprocessing.Process(target=do_backup, ...)

Здесь вы фактически разветвляете и создаете дочерний процесс. Этот процесс является еще одним интерпретатором Python (как и все процессы multiprocessing) и будет выполнять функцию do_backup.

def do_backup()
    subprocess.Popen("rsync ...", ...)

Здесь вы снова разветвляете. Вы создадите еще один процесс (rsync) и позволите ему работать «в фоновом режиме», потому что вы не wait работаете с ним.


После того, как все это прояснилось, я надеюсь, что вы сможете увидеть путь вперед с вашим существующим кодом. Если вы хотите уменьшить его сложность, я рекомендую вам проверить и адаптировать ответ JoErNanO, который повторно использует multiprocessing.Pool для автоматизации отслеживания процессов.

Какой бы способ вы ни выбрали, вам следует избегать разветвления с помощью Popen для создания процесса rsync, так как это создает еще один процесс без необходимости. Вместо этого отметьте os.execv, который заменяет текущий процесс другим

person loopbackbee    schedule 28.10.2014
comment
Спасибо, добавление .wait() к Popen, запускающему rsync, устранило проблему. - person MVanOrder; 28.10.2014

Многопроцессорный пул

Думали ли вы об использовании multiprocessing.Pool ? Они позволяют вам определить фиксированное количество рабочих процессов, которые используются для выполнения нужных вам заданий. Ключ здесь в фиксированном числе, которое даст вам полный контроль над тем, сколько экземпляров rsync вы будете запускать.

Глядя на пример, представленный в документации, которую я связал, сначала вы объявляете Pool из n процессов, а затем можете решить, map() или apply() (с их соответствующими _async() братьями и сестрами) ваша работа в пуле.

from multiprocessing import Pool

def f(x):
    return x*x

if __name__ == '__main__':
    pool = Pool(processes=4)              # start 4 worker processes

    pool.apply_async(f, (10,))    # evaluate "f(10)" asynchronously
    ...
    pool.map(f, range(10))

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

Запуск вашего rsync

Тогда ваш код порождения процесса станет примерно таким:

from multiprocessing import Pool

def do_backup(arg1, arg2, arg3, ...):
    # Do stuff

if __name__ == '__main__':
    # Start a Pool with 4 processes
    pool = Pool(processes=4)
    jobs = []

    for ... :
        # Run the function
        proc = pool.apply_async(func=do_backup, args=(shash["NAME"],ip,shash["buTYPE"], ))
        jobs.append(proc)

    # Wait for jobs to complete before exiting
    while(not all([p.ready() for p in jobs])):
        time.sleep(5)

    # Safely terminate the pool
    pool.close()
    pool.join()
person JoErNanO    schedule 28.10.2014
comment
Вам не хватает волшебства, которое позволит ему фактически запустить процесс, отличный от интерпретатора Python (например, rsync): os.execv или аналогичный - person loopbackbee; 28.10.2014
comment
@goncalopp Я предполагаю, что часть кода уже существует в функции do_backup() OP. :) - person JoErNanO; 28.10.2014
comment
Ваш ответ более питонический, чем мой +1 - person Alex W; 28.10.2014
comment
@JoErNanO Да, конечно, но в первую очередь у него проблемы с do_backup! :) В нынешнем виде, если вы примените свой код к его do_backup, это, вероятно, приведет к тому же поведению, что и он - обратите внимание, что он не вызывает wait, поэтому подпроцессы в Pool возвращаются немедленно, пока rsync все еще работает. - person loopbackbee; 28.10.2014
comment
@goncalopp А, хорошо, теперь я понял. По сути, в его коде он разветвляется дважды: один раз для Popen и один раз для rsync. Таким образом, Popen завершит работу, пока rsync все еще работает. Позвольте мне обдумать это. - person JoErNanO; 28.10.2014
comment
Также может быть полезно упомянуть, что выполнение еще одного POpen (в отличие от execv) приведет к ненужному дублированию количества процессов. - person loopbackbee; 28.10.2014
comment
Это работало для подсчета дочерних процессов, но Popen не любит запускаться, когда я использую пул. Не уверен, почему, я могу изучить это позже. Добавление .wait() на ответ goncalopp устранило проблему. - person MVanOrder; 28.10.2014

Это не многопоточность, а многопроцессорность. Я предполагаю, что вы используете систему Unix, если вы используете rsync, хотя я верю, что она может работать в системах Windows. Чтобы контролировать смерть порожденных дочерних процессов, вы должны fork их.

Есть хороший вопрос о том, как сделать это на Python the-script-is-kille">здесь.

person Alex W    schedule 28.10.2014
comment
Обратите внимание, что subprocess.Popen делает вилку в своем коде. forking очень низкоуровневый и редко встречается в коде Python — существуют библиотеки более высокого уровня, которые помогают в таких задачах (например, multiprocessing, как упоминал JoErNanO) - person loopbackbee; 28.10.2014