При создании программного обеспечения часто возникает необходимость повысить его производительность. Одним из способов достижения этой цели является выполнение нескольких задач одновременно. Об этом подробнее расскажет эксперт, знающий это явление вдоль и поперек — Павел Хошев, автор популярных курсов «Асинхронный Python» и «Многопоточный Python» на Stepik.
В Python есть одно важное ограничение, называемое GIL, расшифровывается как Global Interpreter Lock (Глобальная блокировка интерпретатора). GIL не позволяет достичь истинной одновременной обработки задач. Тем не менее, существуют два подхода, которые могут помочь повысить производительность программного обеспечения, не нарушая правила GIL. Эти подходы называются асинхронное программирование и многопоточное программирование.
Определяемся в терминологии
В этой таблице собраны основные термины из статьи, чтобы облегчить их понимание и помочь систематизировать информацию:
Подход в Асинхронном python
Асинхронный Python использует кооперативную многозадачность, где задачи сами решают, когда уступить контроль другим задачам. Асинхронный код работает с coroutines (это специальный вид функций в Python, которые могут приостанавливаться и возобновляться в произвольный момент времени) и task (представляют собой асинхронные операции, которые могут выполняться параллельно или асинхронно) для управления выполнением программы.
Подход в Многопоточном python
Многопоточный Python оперирует вытесняющей многозадачностью, где операционная система принимает решение о переключении задач. Здесь потоки threads и рабочие процессы workers используются для эффективного управления выполнением программы.
В этой статье мы рассмотрим особенности и различия между этими двумя подходами, чтобы вы могли лучше понять, какой из них лучше подходит для решения вашей задачи.
Зачем переключаться между задачами при написании кода?
Рассмотрим пример: когда вы пишете свой код без использования асинхронности или многопоточности, программа исполняет инструкции поочередно — одну за другой, пока не завершит всю очередь. Но вот загвоздка, когда ваш код работает с сетевыми запросами, он тратит много времени в ожидании ответа от сервера. Именно для таких случаев были разработаны три основные концепции: асинхронность, многопоточность и многопроцессорность (последнюю мы рассматривать не будем). Представьте себе, что это не просто идеи, зародившиеся в уме гениев за обеденным столом. Скорее, это эволюция программирования, неотъемлемая часть его развития.
Теперь, когда вы знакомы с концепциями кооперативной и вытесняющей многозадачности и поняли, в чем их разница, давайте разберемся с GIL (Global Interpreter Lock). Про него можно говорить очень долго. Все еще ведутся обсуждения на наших курсах о плюсах и минусах GIL, о том, как он влияет на выполнение задач и как работает «под капотом».
Если упростить, из-за GIL в Python одновременно может выполняться только одна инструкция байт-кода. То есть хотя в вашей программе может существовать множество потоков, но только один из них активно работает в конкретный момент времени. Этот поток отдает или получает управление в определенные моменты своей работы (подробнее об этом мы рассмотрим позже).
Асинхронный Python
Теперь давайте поговорим о цикле событий и асинхронность.
Важно понимать, что цикл событий это сердце любого асинхронного приложения!
Чтобы лучше понять, как работает асинхронность, представьте себе описание кооперативной многозадачности.
Давайте разберем это на примере. Представьте, что вы проснулись и отправились на кухню, чтобы приготовить завтрак. Ваши задачи таковы:
- Вскипятить воду в чайнике.
- Пожарить омлет.
- Приготовить десерт в духовке.
- Разморозить мясо в микроволновке.
В конечном счете у вас есть четыре задачи, которые могут выполняться в любом порядке и не требуют вашего непрерывного внимания. Это возможно, потому что время выполнения каждой из этих задач, как правило, не ограничено, если вы не следуете жесткой инструкции.
Если вы решите выполнить все задачи в одном потоке без использования асинхронности, вы включите чайник и будете ждать пока он закипит, прежде чем перейти к следующей задаче. Такой подход не всегда эффективен, но именно так работает синхронный код.
Когда вы отправляете все четыре задачи в цикл событий, каждая из них начинает выполняться. Вы нажимаете кнопку на чайнике, затем переключаетесь, чтобы достать 2 яйца из холодильника и отправить их жариться на сковороду. Потом снова переключаетесь — делаете заготовку для десерта и отправляете ее в духовку. То же самое происходит с разморозкой мяса в микроволновке. В каждой из этих задач есть момент, когда для ее выполнения не требуется вашего непосредственного вмешательства, и вы переключаетесь на выполнение другой задачи, которая стоит в очереди. Такая же логика применяется и в цикле событий. Когда задача завершена или достигает момента, когда ей необходимо переключиться на другую задачу (например, вам не нужно контролировать, как кипит вода), вы можете заняться следующим пунктом в списке. Когда чайник закипит или микроволновка разморозит мясо, они уведомят вас. Именно так работает кооперативная многозадачность: цикл событий управляет задачами, получает от них уведомления, обращает на них внимание и, таким образом, контролирует весь процесс работы.
Так и где тут программирование?
Асинхронность и многопоточность идеально подходят для задач ввода/вывода, таких как:
- Сетевые запросы.
- Работа с файлами на жестком диске.
- Запросы к базам данных и т. д.
Представьте, что у вас есть 1000 задач на получение данных с сервера. Когда вы отправляете запрос на получение данных, вам не нужно ждать ответа в текущий момент времени. Вы можете переключиться и отправить новый запрос, пока сервер обрабатывает ваш первый. Вся прелесть асинхронности в том, что цикл событий делает это в автоматическом режиме. От вас лишь потребуется создание задач и распределение ответов.
Многопоточный Python
Для начала стоит понимать, что многопоточность работает по принципу вытесняющей многозадачности, где операционная система управляет переключением задач в потоках. Основной принцип «передачи управления» схож с асинхронностью. Например, когда вы включаете чайник и он больше не требует вашего внимания, вы можете переключиться на другую задачу.
Если провести аналогию с многопоточностью и реальной жизнью, то первой на ум приходит строительная компания. В ней есть директор (пул потоков) и строители (потоки). Каждый сотрудник — это поток, выполняющий определённые задачи. Когда один из строителей не может продолжать работу из-за отсутствия материалов, руководитель направляет его на другой участок, чтобы минимизировать простой. Аналогично, когда поток ждёт ответа от сервера, он может выполнять другие задачи, пока сервер обрабатывает запрос.
Что же выбрать?
Многопоточность подходит для задач, которые можно реализовать в рамках одной функции. Она удобна для многократного выполнения однотипных задач или применения одной функции к каждому объекту в коллекции. Асинхронность, напротив, следует использовать, когда требуется выполнять множество конкурентных задач, связанных с вводом-выводом.
Асинхронное программирование требует принятия асинхронного подхода с самого начала, в то время как многопоточность можно добавить на более поздних этапах. Обо всех преимуществах с примерами кода вы подробнее узнаете в курсах Павла.