Python. Урок 24. Потоки и процессы в Python. Часть 3. Управление процессами

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

Процессы в Python

Процессы в Python позволяют запустить выполнение нескольких задач в параллельном режиме. По сути, при старте процесса запускает еще одна копия интерпретатора Python, в котором выполняется указанная функция. Таким образом, если мы запустим пять процессов, то будет запущено пять отдельных интерпретаторов, в этом случае уже не будет проблем с GIL. Такой способ  позволяет параллельно запускать задачи активно использующие CPU. Они будут распределяться между несколькими процессами (ядрами), что значительно увеличит производительность вычислений.

Класс Process

Классом, который отвечает за создание и управление процессами является Process из пакета multiprocessing. Он совместим по сигнатурам методов и конструктора с threading.Thread, это сделано для более простого перехода от многопотокового приложения к многопроцессному. Помимо одноименных с Thread методов, класс Process дополнительно предоставляет ряд своих. Познакомимся поближе с этим классом, конструктор класса выглядит следующим образом:

class multiprocessing.Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

Параметры конструктора:

  • group
    • Параметр всегда равен None, используется для обратной совместимости с Thread.
  • target
    • Сallable-объект, который будет вызван методом run() при старте процесса.
  • name
    • Имя процесса.
  • args
  • kwargs
    • Параметры объекта, переданного через target.
  • daemon
    • Флаг, определяющий является ли данный процесс демоном (True) или нет (False). Если значение не задано, то оно будет равно свойству daemon родительского процесса.

Помимо методов и свойств, которые совпадают по назначению с аналогичными для класса Thread, класс Process имеет ряд уникальных:

  • pid
    • Возвращает ID процесса. Если процесс ещё не запущен, то вернет None.
  • exitcode
    • Код возврата. Если процесс ещё не завершил свою работу, то вернет None.
  • authkey
    • Ключ аутентификации процесса. При инициализации multiprocessing, с главным процессом связывается специальная строка, которая генерируется с помощью  os.urandom(). Это значение наследуют процессы-потомки, но его можно изменить, задав новое значение, через данное свойство.
  • sentinel
    • Числовой идентификатор, который может использоваться для синхронизации. Подробно об этом будет написано в одном из следующих уроков.
  • close()
    • Освобождает все ресурсы, связанные с процессом. Если процесс еще работает, то вызов метода приведет к выбросу исключения ValueError.

Создание и ожидание завершения работы процессов

Как уже было сказано выше, за организацию работы с процессами в Python отвечает класс Process. Для запуска процесса используется метод start(). Разберем простой пример:

from multiprocessing import Process


def func():
   print("Hello from child Process")


if __name__ == "__main__":
   print("Hello from main Process")
   proc = Process(target=func)
   proc.start()

В рамках данного примера мы написали функцию (строка 4), которая будет выполнена в отдельном процессе, в главном процессе мы создали переменную типа Process (строка 10) с указанием функции func, и запустили его (срока 11). В результате в консоли будут выведены два сообщения из родительского и дочернего процессов:

Hello from main Process
Hello from child Process

За ожидание завершения работы процесса(ов) отвечает метод join, со следующей сигнатурой:

join([timeout])

При вывозе метода join() выполнение программы будет остановлено до тех пор пока соответствующий процесс не завершит работу. Параметр timeout отвечает за время ожидания завершения работы процесса, если указанное время прошло, а процесс еще не завершился, то ожидание будет прервано и выполнение программы продолжится дальше. В случае, если метод join() завершился по таймауту или в результате того, что процесс был завершен аварийно (терминирован), то он вернет None.

Если в приведенном выше примере код главного процесса заменить на следующий:

print("Hello from main Process")
proc = Process(target=func)
proc.start()
print("Goodbye")

То в консоли сообщения будут выведены в таком порядке:

Hello from main Process
Goodbye
Hello from child Process

Это происходит потому, что строка print(“Goodbye”) из основного процесса выполняется раньше, чем print(“Hello from child Process”) из дочернего. Для того, чтобы все было выведено в нужном порядке, воспользуемся методом join():

print("Hello from main Process")
proc = Process(target=func)
proc.start()
proc.join()
print("Goodbye")

Для проверки того выполняется процесс сейчас или нет используется метод is_alive(). Дополним наш пример соответствующими проверками:

print("Hello from main Process")

proc = Process(target=func)
proc.start()

print(f"Proc is_alive status: {proc.is_alive()}")

proc.join()

print("Goodbye")
print(f"Proc is_alive status: {proc.is_alive()}")

Результат выполнения:

Hello from main Process
Proc is_alive status: True
Hello from child Process
Goodbye
Proc is_alive status: False

Создание классов-наследников от Process

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

from multiprocessing import Process
from time import sleep


class CustomProcess(Process):
   def __init__(self, limit):
       Process.__init__(self)
       self._limit = limit

   def run(self):
       for i in range(self._limit):
           print(f"From CustomProcess: {i}")
           sleep(0.5)


if __name__ == "__main__":
   cpr = CustomProcess(3)
   cpr.start()

Мы создали класс CustomProcess, который является наследником от Process и переопределили метод run(), так, что он выводит заданное количество сообщений, которое задается при создании объекта, с интервалом в 500 мс. Запуск процесса осуществляется с помощью метода start().

Принудительное завершение работы процессов

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

  • terminate()
    • Принудительно завершает работу процесса. В Unix отправляется команда SIGTERM, в Windows используется функция TerminateProcess().
  • kill()
    • Метод аналогичный terminate() по функционалу, только вместо SIGTERM в Unix будет отправлена команда SIGKILL.
from multiprocessing import Process
from time import sleep


def func():
   counter = 0
   while True:
       print(f"counter = {counter}")
       counter += 1
       sleep(0.1)


if __name__ == "__main__":   
   proc = Process(target=func)
   proc.start()
   sleep(0.7)
   proc.terminate()

Процессы-демоны

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

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

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

from multiprocessing import Process
from time import sleep


def func(name):
   counter = 0
   while True:
       print(f"proc {name}, counter = {counter}")
       counter += 1
       sleep(0.1)


if __name__ == "__main__":   
   # Указание на то, что процесс демон при создании объекта класса Process
   proc1 = Process(target=func, args=("proc1",), daemon=True)

   # Указание на то, что процесс демон через свойство daemon
   proc2 = Process(target=func, args=("proc2",))
   proc2.daemon = True

   # Запуск процессов
   proc1.start()
   proc2.start()
   sleep(0.3)

   # Процессы proc1 и proc2 завершаться вместе с родительским процессом
   # ...

При запуске данной программы на консоли должно появиться следующее:

proc proc1, counter = 0
proc proc2, counter = 0
proc proc1, counter = 1
proc proc2, counter = 1
proc proc1, counter = 2
proc proc2, counter = 2

P.S.

Вводные уроки по “Линейной алгебре на Python” вы можете найти соответствующей странице нашего сайта. Все уроки по этой теме собраны в книге “Линейная алгебра на Python”.
Книга: Линейная алгебра на Python
Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas.  Для начала вы можете познакомиться с вводными уроками. Все уроки по библиотеке Pandas собраны в книге Pandas. Работа с данными”.
Книга: Pandas. Работа с данными

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *