python
October 9, 2019

Multiprocessing в Python

Немного истории

Модуль multiprocessing изначально был добавлен в Python 2.6. Этот модуль позволяет создавать процессы таким же образом, как при создании потоков при помощи модуля threading.

Суть в том, что, в связи с тем, что теперь мы создаем процессы, мы можем обойти GIL (Global Interpreter Lock) и воспользоваться возможностью использования нескольких процессоров (или даже ядер) на компьютере.

Пакет multiprocessing также включает ряд API, которых вообще нет в модуле threading. Например, там есть очень удобный класс Pool, который мы можем использовать для параллельного выполнения функции между несколькими входами. Мы рассмотрим Pool немного позже. А начнем пожалуй с класса Process модуля multiprocessing.

Приступим к работе с Multiprocessing

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

import os
from multiprocessing import Process

def func(number):
    """
        Выведем число, возведенное в квадрат
    """
    proc = os.getpid()
    print(f'{number} squared to {number**2} by process id: {proc}')

if __name__ == '__main__':
    numbers = [1, 2, 3, 4, 5]
    procs = []

    for number in numbers:
        proc = Process(target=func, args=(number,))
        procs.append(proc)
        proc.start()

    for proc in procs:
        proc.join()

Вывод будет таким:

1 squared to 1 by process id: 1600
2 squared to 4 by process id: 1601
3 squared to 9 by process id: 1602
4 squared to 16 by process id: 1603
5 squared to 25 by process id: 1604
Здесь хотелось бы обсудить несколько важных моментов. Во-первых, f-строки работают только в версиях Python 3.6 или выше. Во-вторых, аргументы в функции Process это всегда кортеж, даже если там всего один элемент.

Внутри функции func, мы возводим переданное число в квадрат. Мы также используем модуль os, чтобы получить ID текущего процесса. С помощью него можно определить какой именно процесс вызывает функцию.

Далее, в нижнем блоке кода, мы создаем несколько процессов и стартуем их с помощью функции start(). Самый последний цикл только вызывает метод join() для каждого из процессов, что говорит Python подождать, пока процесс завершится. Если вам нужно остановить процесс, вы можете вызвать метод terminate().

Иногда приятно иметь читабельное название процессов. К счастью, multiprocessing дает возможность получить доступ к названию нашего процесса.

from multiprocessing import Process, current_process

def func(number):
    """
        Выведем число, возведенное в квадрат
    """
    proc = current_process().name
    print(f'{number} squared to {number**2} by process {proc}')

if __name__ == '__main__':
    numbers = [1, 2, 3, 4, 5]
    procs = []

    for number in numbers:
        proc = Process(target=func, args=(number,))
        procs.append(proc)
        proc.start()

    for proc in procs:
        proc.join()

Вывод будет таким:

1 squared to 1 by process Process-1
2 squared to 4 by process Process-2
3 squared to 9 by process Process-3
4 squared to 16 by process Process-4
5 squared to 25 by process Process-5

Кроме того, мы можем напрямую назначить имя нашему процессу:

proc = Process(target=func, name='My Process', args=(number,))

Класс Pool

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

from multiprocessing import Pool

def func(number):
    return number**2

if __name__ == '__main__':
    numbers = [1, 2, 3]
    pool = Pool(processes=3)
    print(pool.map(func, numbers))

Здесь мы создали экземпляр Pool и указали ему создать три рабочих процесса. Далее мы используем метод map для отображения функции для каждого процесса. Наконец мы выводим результат, что в нашем случае является списком: [1, 4, 9].

Реальный пример

Попробуем применить метод requests.get() к некоторому списку сайтов. Последовательное выполнение задачи отнимет много времени, но что если сделать это параллельно?

import requests
from multiprocessing import Pool

urls = [
    'http://www.python.org',
    'http://www.python.org/about/',
    'http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html',
    'http://www.python.org/doc/',
    'http://www.python.org/download/',
    'http://www.python.org/getit/',
    'http://www.python.org/community/',
    'https://wiki.python.org/moin/',
    'http://planet.python.org/',
    'https://wiki.python.org/moin/LocalUserGroups',
    'http://www.python.org/psf/',
    'http://docs.python.org/devguide/',
    'http://www.python.org/community/awards/'
    ]
# создадим пул работников
pool = Pool(4)

# получим результат с помощью функции map
results = pool.map(requests.get, urls)

# закроем пул и подождем завершения работы 
pool.close()
pool.join()

Теперь сравним скорости обработки при различном размере пула:

# ------- 13 Pool ------- #
pool = Pool(13)
results = pool.map(requests.get, urls)

# ------- 8 Pool ------- #
pool = Pool(8)
results = pool.map(requests.get, urls)

# ------- 4 Pool ------- #
pool = Pool(4)
results = pool.map(requests.get, urls)

# ------- Single ------- #
results = []
for url in urls:
    result = requests.get(url)
    results.append(result)
Вообще говоря, скорость выполнения таких процессов сильно зависит от скорости и качества интернет соединения. Кроме того, логически более правильно здесь было бы использовать async а не multiprocessing. Однако даже в таком примере виден прирост скорости.

На моей машине получилось вот так:

13 Pool: 2.38 sec
8 Pool: 2.46 sec
4 Pool: 2.49 sec
Single process: 6.99 sec

Итог

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

Мы затронули только некоторые простые вопросы, связанные многопроцессорным программированием на Python. Разумеется, в документации Python представлено намного больше развернутой информации, так что рекомендую с ней ознакомиться.

P.S. Не забывайте делиться статьями и вступать на мой канал hello world. Это поможет выпускать больше качественного материала.