Задать вопрос

Почему Python multiprocessing нестабилен?

Здравствуйте сообщество. Попытки разобраться с многопроцессорностью на питоне привели к серьезной загвоздке. Буду очень признательна за любую помощь в разборе проблемы, или за рекомендации по исправлению кода.
В программе для математических расчетов использую часть с multiprocessing. Параллельный блок кода выглядит так:
worker_count = multiprocessing.cpu_count()
        jobs = []
        print "---> Starting multiprocessing #", series_number
        for i in xrange(worker_count):
            s = solver.get_solution(copied_system)
            p = multiprocessing.Process(target=combinatorial_set.find_nearest_set_point, args=(s, result_queue))
            jobs.append(p)
            p.start()
            print p
        for w in jobs:
            w.join()
            print w
        results = []
        while not result_queue.empty():
            results.append(result_queue.get())
        for i in xrange(len(res)):
            print results[i], is_solution(copied_system, results[i])
            if is_solution(copied_system, results[i]):
                func_value = f(results[i])
                experiment_valid_points[func_value] = results[i]

        #End of parallel
        print "---> End of multiprocessing #", series_number


Вот что должен был делать данный код:
Проводится серия экспериментов. В рамках каждого эксперимента совершаются такие действия
1. Запуск эксперимента.
2. Генерация точек (s) в некоторой области. Количество точек = количеству ядер.
3. Запуск для каждой точки нахождения минимума функции. Эта часть выполняется параллельно.
4. Сбор результатов.
5. Выбор из сгенерированых точек лучшей.
6. Пересчет области для генерации.
7. Переход к новому эксперименту.

А вот что получается:
На практике данный код работает очень нестабильно. На одних и тех же данных могут быть следующие ситуации.
1. Первая "идеальная": в рамках каждого эксперимента запускается 4 python.exe которые каждый загружают ~ по 25% ЦП. После окончания эксперимента 3 из этих четырех процессов умирают и все начинается с начала.
2. Вторая "хуже" : в рамках каждого эксперимента запускается 4 python.exe но они выполняются псевдопараллельно. В мониторе ресурсов видно 4 процесса python.exe по одному потоку в каждом. В один момент времени работает всегда только один, остальные прерваны пока до них не дойдет очередь.
3. Третий "худший" : в консоли все 4 процесса отписываются Process(Process-i, started), но ни в диспетчере задач, ни в мониторе ресурсов 4 процессов нет. Есть только 1 python.exe, который загружает ~ 25% ЦП и не понятно что считает. Естественно работа дальше не идет, так как нужно дождаться завершения работы всех четырех процессов, а их просто не существует.

Заранее спасибо за ответы.
  • Вопрос задан
  • 4524 просмотра
Подписаться 4 Оценить Комментировать
Решения вопроса 1
@nirvimel
Хорошо, что вы указали в тегах Windows, это все объясняет. Под Windows нет простого способа "раздвоиться" процессу при вызове multiprocessing.Process, поэтому осуществляется очень сложная эмуляция этого поведения. При этом функция target выдирается из модуля, запускается в отдельном интерпретаторе, а все параметры сереализуются передаются и десереализуются перед вызовом target, при этом инициализация модуля в новом интерпретаторе выполняется частично (инициализируется только глобальный контекс). Подробнее об этом, например, тут, есть еще одна очень хорошая статья где подробно рассмотрен этот механизм, но сейчас не найду ссылку.

Коротко о том, как готовить multiprocessing под Windows:
  1. Разделять процессы (вызов multiprocessing.Process()) как можно раньше в коде.
  2. По возможности избегать инициализации любых ресурсов и глобальных переменных до разделения. Учитывайте, что этот код выполняется во всех процессах независимо и может давать кучу сторонних эффектов.
  3. Не передавать через args никаких сложных объектов с "поведением" (кроме объектов из самого multiprocessing, он сам знает как их правильно передавать), только голые данные (примитивы или объекты состоящие только из примитивов), которые сериализуются без сторонних эффектов.
  4. Создавать дочерние процессы один раз, и на протяжении всего времени работать с ними посредством обмена сообщениями через Pipe/Queue. Не порождать новые процессы в цикле вычислений в момент "когда понадобятся".
  5. Queue при попытке записи/чтения может блокировать процесс, если при этом происходит запись/чтения в/из нее в другом процессе. (Думаю, именно это и происходит в коде в вопросе).
  6. Лучше использовать Pipe, который в худшем случае блокирует один процесс, а не все, как Queue.
  7. При создании процесса можно передавать ему два Pipe (input одного + output другого), в вызывающем процессе хранить соответствующие им коннекторы и только при помощи их общаться с дочерним процессом.
  8. Можно не делать process.join(), а просто читать результаты из output Pipe, они прочтутся только после того как попадут туда, что дальше будет происходить с процессом уже не важно (можно поставить return после записи в Pipe в дочернем процессе).
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
Olej
@Olej
инженер, программист, преподаватель
3. Третий "худший"

1. Что-то мне кажется, что в вот этом target=combinatorial_set.find_nearest_set_point - у вас должна быть критическая ошибка, которая просто убивает процессы.

Что на размерности 30 превращается в адову кучу действий.

2. Чего же вы при этом делаете это на Python? Вы при этом теряете раз 100 в производительности, в сравнении с языками, компилирующими в нативный код (C, C++, Go). Тем более, что там бы вы имели возможность использовать легковесные потоки взамен тяжёлых параллельных процессов.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы