Введение
В этом цикле статей будет рассмотрено асинхронное программирование при помощи сопрограмм в языке Python. В данной мы рассмотрим основные понятия и термины, которыми будем оперировать в дальнейшем, вкратце познакомимся с историей асинхронного программирования и состоянием дел в этой области на сегодняшний день. Также Вы узнаете о том, что такое сопрограммы и чем они могут быть полезны при написании кода в асинхронном стиле.
Во второй статье будет рассмотрена реализация сопрограмм при помощи расширенных возможностей генераторов в Python (PEP 342), в третьей мы рассмотрим модуль asyncio (PEP 3156), который стал частью стандартной библиотеки в Python 3.4 и доступен в виде отдельного пакета для Python 3.3, а четвёртая статья цикла будет посвящена асинхронным функциям и сопрограммам в Python 3.5 с использованием нового синтаксиса async/await (PEP 0492).
Понятие асинхронного программирования и сопрограмм
Наверное, сегодня все уже слышали о Node.js и знают причины возрастания его популярности: один язык для фронтенда и бекенда (что в рамках данной статьи нас не интересует) и то, что он является платформой для построения асинхронных неблокирующих веб-серверов.
Другой известной технологией, основанной на данной модели, является веб-сервер nginx, который часто используется на высоконагруженных проектах, занимая первое место по частоте использования среди 10000 самых посещаемых сайтов в мире (согласно данным W3Techs).
Так что же такое асинхронное программирование и почему оно становится таким популярным, особенно в highload-проектах?
На самом деле, асинхронное программирование существовало ещё на заре вычислительной техники, так как было важно максимально использовать аппаратные ресурсы машины. Но не так давно оно стало чуть ли не стандартной парадигмой программирования, настолько, что можно сказать, что большинство написанных в наши дни приложений являются асинхронными объектно-ориентированными программами.
Давайте для наглядности рассмотрим это на примере графических интерфейсов пользователя. Что происходит, когда пользователь не производит никаких действий? Ничего. Программа должна ждать, пока пользователь укажет ей, что делать.
Это ожидание можно реализовать в виде постоянных проверок: «а не нажал ли пользователь на кнопку?», «а не поставил ли пользователь курсор в поле ввода?». Таким образом, вычислительные ресурсы тратятся просто на то, чтобы проверить, не случилось ли что-нибудь. К счастью, практически все UI-фреймворки построены иначе. Они реализуют систему обработки событий. Любое действие пользователя – это событие, и разработчик может привязать к нему код – обработчик события.
Это настолько привычный паттерн, что многие разработчики даже не задумываются, как он работает, хотя следовало бы. Например, представьте, что на экране есть три кнопки и пользователь каким-то образом нажимает на них практически одновременно. Запустятся ли три разных обработчика событий?
Как правило, ответ – нет. Наиболее распространённая архитектура системы обработки событий – однопоточная, лишь с одним потоком исполнения.
Однопоточные системы обработки событий практически всегда реализуются при помощи очереди событий или сообщений. Идея состоит в том, что, когда программа ничего не делает, её поток смотрит на наличие новых событий в очереди. Когда происходит событие, оно добавляется в очередь и, если поток исполнения в это время не делает ничего другого, он забирает следующее событие из очереди и выполняет соответствующий обработчик.
Таким образом, события добавляются в очередь и UI-фреймворк предоставляет диспетчер, который выполняется в том же потоке, что и обработчики, вызывая их по мере надобности. В любой момент времени поток находится либо в каком-то обработчике события, либо в диспетчере, ожидая следующего события.
Відео курси за схожою тематикою:
Возникает логичный вопрос: каким же образом событие попадает в очередь, если поток исполнения занят обработкой другого события? Дело в том, что у операционной системы много потоков и тот код, который действительно взаимодействует с пользователем, выполняется отдельно от нашей программы и лишь посылает ей сообщения.
Это пример асинхронной системы, так как мы не знаем, в каком порядке будет выполнятся код. Обработчики событий, с точки зрения программы, могут выполняться произвольно.
Но в данной модели обработчики событий являются неделимыми действиями. И тут возникает проблема: если обработчик событий выполняется слишком долго, интерфейс как бы «подвисает». Причина в том, что пока обработчик события не вернул управление в диспетчер, следующий обработчик не будет выполнен.
Решением является минимизировать количество работы, которое выполняет обработчик события. Но что если ему требуется совершить какие-то вычисления или загрузить данные с сервера? Очевидный ответ – выполнять обработчик этого события в отдельном потоке. Однако в JavaScript есть лишь один поток исполнения, а в Python, как известно, проблемой многопоточных приложений, которая значительно их замедляет, является Global Interpreter Lock (GIL).
Тут мы подходим к тому, что существует два вида многозадачности: вытесняющая и кооперативная.
Потоки и процессы используют вытесняющую многозадачность. Это значит, что операционная система производит квантование времени и постоянно переключается между разными потоками, сохраняя и восстанавливая их контекст выполнения.
При использовании кооперативной многозадачности ветви кода, которые исполняются параллельно, сами отдают управление в определённые моменты времени. Кооперативная многозадачность как способ одновременного выполнения отдельных программ устарела и не используется в современных операционных системах, однако, идеи, заложенные в неё, оказываются очень полезными для организации выполнения асинхронного кода и позволяют при грамотном использовании максимально использовать вычислительные ресурсы в рамках одного потока (а при комбинировании этого подхода с традиционной многопоточностью, как в async/await в C#, можно строить крайне эффективные приложения).
Можно построить обработчик события из множества асинхронных функций обратного вызова (callback-функций), которые управляются общим циклом событий, как это делается в Node.js, однако такой код сложно отлаживать и поддерживать. Значительно упрощают его паттерны Promise и Future, однако Python и некоторые другие языки программирования поддерживают механизм, который позволяет в данном случае обойтись без callback-функций – сопрограммы.
Безкоштовні вебінари за схожою тематикою:
Сопрограмма (coroutine) – это компонент программы, обобщающий понятие подпрограммы, который дополнительно поддерживает множество входных точек (а не одну, как подпрограмма) и остановку и продолжение выполнения с сохранением определённого положения.
Сопрограммы в данном случае удобны тем, что позволяют писать асинхронный код в синхронном стиле. В последующих статьях мы рассмотрим механизмы их реализации в Python.
Статті за схожою тематикою