Асинхронное программирование для начинающих (Часть 1)

Многие начинающие разработчики слышали об асинхронном программировании, но абсолютно не знают, что это такое. А некоторые даже пытались создавать асинхронные программы, но сталкивались с проблемами. 

Что такое асинхронное программирование?

Чтобы понять, что такое асинхронное программирование, необходимо сначала разобраться, что такое синхронное. Стандартная программа, с которой начинает работу каждый новичок в Python, – это представитель синхронного типа. Она выполняется поэтапно. 

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

Вот некоторые варианты синхронных приложений:

  1. Программы для пакетной обработки. Они, как правило, синхронные. Сервер получает определенную информацию от клиента, обрабатывает ее, а потом создает определенный вывод. И этот процесс состоит из нескольких шагов, которые сменяют друг друга до тех пор, пока не будет достигнут желаемый результат. Все, что нужно делать во время написания кода – это следить за тем, какие этапы и насколько правильный их порядок. 
  2. Программы для командной строки. Это крошечные процессы, которые выполняются из терминала. Данный тип скриптов может выполняться, чтобы создавать или трансформировать что-то, генерировать отчеты либо создавать список данных. Для выполнения любой из этих задач требуется серия шагов, которые осуществляются последовательно вплоть до полного завершения работы приложения.

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

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

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

Как создать синхронный веб-сервер

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

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

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

Другой подход к Python-разработке

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

Давайте приведем некоторые примеры того, какие процессы являются синхронными, а какие – асинхронными. Это поможет понять принцип асинхронного программирования.

  1. Подсчет коммунальных услуг – синхронная задача. Пока не будет оплачен счет, придется выполнять череду последовательных действий. В данный момент вы полностью контролируете процесс, выбирая действия, которые дальше следует выполнить. 
  2. Тем не менее, вы всегда можете сделать паузу в расчетах и заправить стиральную машину. Далее можно высушить белье и новую партию снова загрузить в стиральную машину.

В этих примерах и подсчет коммунальных услуг, и работа со стиральной машиной – примеры синхронных задач, которые вы выполняете шаг за шагом:

  1. Посчитали квитанцию 1.
  2. Посчитали квитанцию 2.
  3. Прервались, загрузили стиральную машину.
  4. Посчитали квитанцию 3.

Но, в момент, когда вы начали считать квитанцию 3, стиральная машина продолжает работать. И получается, что стирка стала асинхронной задачей. Вы себе считаете коммунальные услуги, а стиральная машина выполняет свою задачу. Эти два процесса происходят одновременно.

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

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

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

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

Но несмотря на то, что контекст изменился, через некоторое время звуковой сигнал даст вам понять, что стирка закончилась.

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

Программирование родительского элемента: не так уж и легко!

Если описанный выше пример вам знаком, это прекрасно. Ведь в этом случае понять, как работает асинхронное программирование, будет значительно проще. Попробуем сделать несколько мысленных экспериментов.

Синхронный родитель

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

Если сменить приоритет на, например, коммунальные платежи, то тогда не получилось бы присматривать за детьми. То есть, какими бы ни были приоритеты у задач, только одна из них может осуществляться в один момент времени. 

Родитель опросник

Можно попробовать воспользоваться опросником (polling). Но в этом случае все равно время от времени необходимо отвлекаться от задачи и проверять, не стоит ли уделить внимание какой-то другой из них. 

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

Как видим, в этом подходе есть несколько проблем:

  1. Родителю придется проверять то, на что и не нужно обращать внимание в тот момент. Например, стиральная машина все еще работает, дети благополучно играются и так далее.
  2. Родитель может слишком поздно среагировать на новую задачу, которая перед ним внезапно всплыла. Например, если стирка началась в самом начале опросника, то к моменту следующего чекпоинта одежда может помяться. А учитывая то, что в предыдущем примере наивысший приоритет – это присмотр за детьми, вывод напрашивается сам собой.

Конечно, можно уменьшить интервал опросника. Тем не менее, в этом случае придется тратить больше ресурсов на переключение. 

Родитель потока

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

Все пространство памяти распределяется между потоками. Поэтому они являются довольно автономными и не зависят от ресурсов других.

Если каждая из задач вами рассматривается, как составляющая единого приложения, то вполне можно реализовать их с помощью потоков. Проще говоря, можно сделать «дубликат» родителя, который будет заниматься каждой задачей по отдельности – следить за детьми, работой стиральной машины, подсчетом коммунальных услуг. И каждый из этих двойников будет работать автономно.

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

Предположим, родителю А нужно следить за сушкой, а родителю Б – за стиркой. Задача родителя А – когда вещи будут сухими, убрать их и развесить новые. Задача родителя Б – следить за стиральной машиной и вытаскивать одежду, когда она закончит процесс стирки. Получается, что второму родителю нужно также воспользоваться сушилкой, которая в данный момент занята родителем А. А он, в свою очередь, хочет перенести вещи на пустую сушилку, а для этого нужно заняться стиральной машиной. 

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

Часть 2 статьи ⇒

ОфисГуру
Adblock
detector