python_parallel

Вас много, а я один: параллелизм в Python

Наверное, самая частая претензия к Python состоит в его низкой производительности. И действительно, в бенчмарках этот язык проигрывает JavaScript, C++/C#, Go и другим конкурентам. Кроме того, в самой его архитектуре предусмотрены ограничения, которые ограничивают возможности параллельных вычислений.

Специализация Full-stack веб-разработчик на Python
Идет набор в группу 5 900₽ в месяц

Сегодня мы разберемся, как вообще в программировании организуется одновременное выполнение нескольких задач и действительно ли Python так уж отстает в этом от других языков. Всем, кто планирует учить Python, чтобы заниматься веб-разработкой, работать с базами данных или фронтендом, эта статья поможет уяснить, как этот язык справляется со многими ресурсоемкими задачами. Тем же, кто учит программирование с нуля, наш материал будет особенно полезен, поскольку мы поговорим о фундаментальных материях, благодаря которым компьютер может работать с программами и приложениями.

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

Теперь они будут работать параллельно. Центральный процессор функционирует точно так же — если у него больше одного ядра, он может распределять нагрузку между ними, обеспечивая параллелизм операций.

Специализация «Frontend-разработчик»
Идет набор в группу 5900₽ в месяц

Если же знакомый отказывается помочь нашему герою, тому приходится действовать в одиночку. Как это нередко бывает, он начинает делать все сразу: убрал одну рубашку в шкаф, на обратном пути сложил в ящик часть бумаг, тут же поднял с пола какой-то мусор, понес его в ведро, захватив со стола ненужные чеки… 

Человек переключается с одной задачи на другую, понемногу продвигая каждую из них к финалу. Можно сказать, что задачи конкурируют за его внимание и ресурсы. Именно поэтому в компьютерной науке это явление называется конкурентностью. Одноядерный процессор может подходить к выполнению нескольких операций только таким образом — ему некого звать на подмогу. Он выделяет время каждому отдельному потоку и переключается между ними, не дожидаясь завершения задачи.

Итак, особенность Python в том, что он организует потоки вычислений на конкурентной основе. За это отвечает специальный механизм — глобальный блокировщик интерпретатора (Global Interpreter Lock). Он следит за тем, чтобы в каждый момент времени в байт-коде Python выполнялся только один поток. В ранних версиях Python переключение между потоками происходило по достижении заданного количества операций, сейчас GIL снимает блокировку через определенное количество секунд. Зачем же нужно это ограничение?

Без GIL при выполнении приложения может возникнуть ситуация, когда два потока одновременно обратятся к одной ячейке памяти. Как несложно догадаться, попытка записать разные данные в одну ячейку или прочитать информацию в момент ее записи приведет к критической ошибке. Применение глобального блокировщика исключает эту угрозу, позволяя добиться потоковой безопасности (thread safety). Разработчики получают возможность работать с непотокобезопасными библиотеками на C, хоть за это и приходится расплачиваться ограничениями скорости.

Анализ Данных: курс-тренажер по SQL
Идет набор в группу 1 600₽ в месяц

Чтобы увидеть механизм потоков в действии, можно использовать следующий код:

import threading
import time
import random


def worker(number):
    sleep = random.randrange(1, 10)
    time.sleep(sleep)
    print("I am Worker {}, I slept for {} seconds".format(number, sleep))


for i in range(5):
    t = threading.Thread(target=worker, args=(i,))
    t.start()

print("All Threads are queued, let's see when they finish!")

Результаты, очевидно, будут разными в каждом случае, но картина получится примерно такая:

$ python thread_test.py
All Threads are queued, let's see when they finish!
I am Worker 1, I slept for 1 seconds
I am Worker 3, I slept for 4 seconds
I am Worker 4, I slept for 5 seconds
I am Worker 2, I slept for 7 seconds
I am Worker 0, I slept for 9 seconds

Стоит отметить, что GIL внедрен по умолчанию в стандартной реализации Python (CPython), однако при необходимости разработчики могут воспользоваться реализациями без этого механизма (Jython, IronPython).

Так неужели один из самых популярных на сегодня языков программирования неспособен справиться с несколькими задачами одновременно? Разумеется, все не так плохо — разработчики добавили модуль multiprocessing, который обеспечивает (ни за что не догадаетесь) многопоточность.

Модуль распределяет процессы между ядрами CPU (да, на одноядерном процессоре это не сработает). Как можно прочитать в официальной документации, это API использует субпроцессы вместо потоков, обходя таким образом ограничения GIL. С помощью класса Pool разработчик может распределять выполнение одной функции между несколькими процессами для разных входных значений.

Пример применения multiprocessing из все той же документации:

from multiprocessing import Pool

def f(x):
    return x*x

if __name__ == '__main__':
    p = Pool(5)
    print(p.map(f, [1, 2, 3]))

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

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

Текст: Помогаев Дмитрий

Поделиться:
Опубликовано в рубрике PythonTagged

SkillFactory.Рассылка