Параллельные вычисления – инструменты

В языке Python присутствует ряд инструментов по написанию асинхронного кода. Я хочу рассмотреть их концептуально.

В первую очередь, опеределим две задачи, которые хотим достичь:

  • Сделать больше за единицу времени. По факту эту ускорение ввода/вывода в программе: пока у нас функция висит и ждёт отклика от внешней базы данных или от сетевого порта, мы пускаем другие задачи на выполнение. Благодаря особенности Python (наличие Global Interpretor Lock – GIL), каждая подзадача или нить исполняется строго последовательно одна за другой. То есть, нельзя запустить паралелльно несколько нитей на исполнение. Но зато можно, пока одна нить ждёт, исполнить другую-третью;
  • Сделать быстрее задачу. Это прямое ускорение ресурсоёмкой (по отношению к ЦПУ) задачи. Несмотря на наличие GIL, в Python можно ускорить исполнение задач целым рядом способов.

Получаем такую диаграмму:

Способы ускорить код в Python

Начну с ускорения ввода/вывода. Классический подход с помощью нитей (threads) подразумевает, что в коде можно обозначить сопрограммы, пустить их отдельными нитями и далее в какой-то момент ждать результат от всех исполненных сопрограмм, и собрать все результаты вместе.

В случае же с asyncio, всё гораздо проще и интереснее: поток/нить у нас ровно одна, но операции в рамках этого процесса мы разбиваем на кусочки. Например, на картинке ниже у нас начинает работать голубая функция, которая повисает в ожидании отклика. Мы же не теряем времени и запускаем другие маленькие задачи (красную и зелёную) прямо в этом потоке, пока синяя задача у нас находится в состоянии ожидания (await).

Нити и асинхронное исполнение

В Python можно достичь настоящей многоядерности в объезд GIL. Просто, для этого нужно оперировать не потоками/нитями, а процессами/ядрами. Это по сути отдельные процессы-программы, которые имеют свой интерпретатор, область памяти для исполнения, и GIL на процесс деления на процессы не влияет. Сущности это более тяжёлые, нежели нити, поэтому разделение процесса на два не принесёт чистого двукратного выигрыша в производительности, и ощутимый процент в принципе будет съедаться процедурой распараллеливания, но игра определённо стоит свеч.

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

Leave a Reply

Your email address will not be published. Required fields are marked *