Вы можете использовать Python в работе и даже не подозревать о существовании асинхронного программирования. Но если вам действительно интересно, как все устроено изнутри, с этим вопросом стоит разобраться подробнее.
Асинхронность — это возможность выполнения программой задач и процессов без ожидания их завершения.
То есть если предыдущий процесс все еще находится на этапе выполнения, асинхронная программа может легко перейти к обработке следующих задач.
Для чего нужна асинхронность? Программы, которые выполняются последовательно, просты для понимания. В них все процессы выполняются шаг за шагом. Но для решения некоторых практических задач в современном программировании такой подход не всегда себя оправдывает, а потому приходится применять другие методы разработки. Асинхронное программирование усложняет программы, но с его помощью можно их оптимизировать и повысить эффективность. Оно позволяет всем задачам в вашем коде выполняться одновременно (этого синхронные процессы обеспечить не могут).
Асинхронное программирование может быть полезным, если:
Как уже было сказано выше, не всегда поочередное выполнение строк кода бывает эффективным. Проблему последовательности могут решить так называемые потоки (threads). Благодаря им программа может выполнять несколько задач одновременно.
Многопоточные программы представляют собой более сложную структуру, а потому больше подвержены ошибкам и сбоям. Из наиболее распространенных — ресурсное голодание, взаимная блокировка, состояние гонки. Чтобы не допускать ошибок можно пойти на курс от Mate Academy и выучить все тонкости с практикующими специалистами.
Асинхронный ввод-вывод использует один поток в одном процессе и дает ощущение параллельного выполнения и многозадачности. Он позволяет справиться с проблемами потоков, но на самом деле предназначен для так называемого переключения контекста процессора. Что это такое? При наличии нескольких потоков каждое из ядер процессора способно запустить лишь один поток. Для того, чтобы все процессы были запущены одновременно и совместно расходовали ресурсы, процессор вынужден переключать контекст. Он запоминает контекст одного потока и переключается на другой. Периодичность переключений определяется самим процессором.
Асинхронное программирование потоково обрабатывает пользовательское пространство. Здесь уже не процессор участвует в переключении контекста, а само приложение, и происходит это в заранее заданных точках.
Зеленые потоки позволяют очень быстро писать асинхронный код. Особенность их использования заключается в том, что переключения между greenlets происходит не в процессоре, а в приложении.
Зеленые потоки имеют простую структуру и позволяют применять в Python совместную многопоточность. Довольно часто для применения зеленых потоков используется Python-библиотека Gevent. Она способна изменить поведение стандартных библиотек для выполнения неблокирующих операций ввода-вывода.
Пример. Обращение к трем URL-адресам одновременно с использованием библиотеки Gevent:
import gevent.monkey from urllib.request import urlopen gevent.monkey.patch_all() urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org'] def print_head(url): print('Starting {}'.format(url)) data = urlopen(url).read() print('{}: {} bytes: {}'.format(url, len(data), data)) jobs = [gevent.spawn(print_head, _url) for _url in urls] gevent.wait(jobs)
Особенность библиотеки Gevent заключается в том, что API-интерфейс использует не потоки, а сопрограммы (coroutines):
Сопрограммы содержат в себе команды для возвращения событий в очередь при необходимости.
Помимо распространенных библиотек для асинхронного программирования в Python — Gevent и Asyncio — существует не менее известная — Tornado. В ней для асинхронного ввода-вывода используется функция обратного вызова.
Пример. Обращение к трем URL-адресам одновременно с использованием библиотеки Tornado.
import tornado.ioloop from tornado.httpclient import AsyncHTTPClient urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org'] def handle_response(response): if response.error: print("Error:", response.error) else: url = response.request.url data = response.body print('{}: {} bytes: {}'.format(url, len(data), data)) http_client = AsyncHTTPClient() for url in urls: http_client.fetch(url, handle_response) tornado.ioloop.IOLoop.instance().start()
где:
Благодаря используемому в коде методу AsyncHTTPClient.fetch информацию об URL-адресе можно получать без блокировки. Каждая последующая строка выполняется еще до того, как получен ответ по URL, а значит, результат выполнения получить невозможно. fetch возвращает объект, вызывая функцию обратного вызова — handle_response.
Обратный вызов в асинхронном программировании, пожалуй, единственный способ избежать блокировки. Именно по этой причине может возникать длинная цепочка действий, состоящая из функций обратного вызова, безостановочно сменяющих одна другую. Каждый обратный вызов — это отдельный поток, и он не всегда может принять все объекты, особенно если используются сторонние API-интерфейсы.
Генераторы позволяют обрабатывать огромные потоки данных. Представим, что у вас есть файл немыслимых размеров и вам необходимо обработать и вычленить необходимую информацию. Маловероятно, что локально у вас будет достаточно места и памяти на личном ПК для обработки огромного объема данных, если только вы не станете делать это частями. В Python обработкой больших массивов данных занимаются генераторы.
Генераторы не вычисляют значения сразу всех элементов, а сохраняют только последний вычисленный, а также условия и правило перехода к следующему. Следующее значение вычисляется только при выполнении метода next(). Предыдущая информация стирается.
Пример. Создание generator object gen.
>>> a = (i**2 for i in range(1,5)) >>> a <generator object <genexpr> at 0x0000023A7524D6D0> >>> next(a) 1 >>> next(a) 4 >>> next(a) 9 >>> next(a) 16 >>> next(a) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
При каждом вызове next(a) значения генератора 1, 4, 9, 16 будут рассчитываться по одному. Все предыдущие данные будут удалены.
Если вызвать next(gen) еще раз, генератор удалит последнее значение (в нашем примере — 16) и завершит весь процесс исключением StopIteration:
>>> next(gen) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
Чтобы не попасть на блокировку ввода-вывода, следует использовать или асинхронное программирование, или многопоточность. Python предоставляет возможность использования зеленых потоков или функцию обратного вызова:
Эти проблемы способны решить генераторы, о которых мы упоминали выше. Благодаря им функции могут возвращать по одному элементу списка за один раз. Выполнение будет приостанавливаться ровно до момента запроса следующего элемента. В работе генераторов тоже есть свои нюансы — они полностью зависят от той функции, которая их вызывает. Однако это проблема решается синтаксисом yield from. Благодаря чему генераторы способы получать результаты друг друга, создавать исключения и поддерживать стек.
На этом принципе и была создана Asyncio — асинхронная библиотека, где в цикле событий могут запускаться генераторы. Чтобы в сопрограмму был добавлен генератор, здесь необходимо всего лишь добавить декоратор @coroutine.
Пример. Обращение к трем URL-адресам одновременно с использованием библиотеки Asyncio.
import asyncio import aiohttp urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org'] @asyncio.coroutine def call_url(url): print('Starting {}'.format(url)) response = yield from aiohttp.get(url) data = yield from response.text() print('{}: {} bytes: {}'.format(url, len(data), data)) return data futures = [call_url(url) for url in urls] loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(futures))
Теперь все ошибки передаются в стек правильно, обратных вызовов нет, запускаются все сопрограммы, при необходимости объект всегда можно вернуть, последующая строка не выполняется, пока полностью не будет выполнена предыдущая.
Благодаря своей универсальности и мощности библиотека Asyncio стала основополагающей в Python. Здесь для универсальности, более точного понимания асинхронного кода и разделения методов и генераторов используются ключевые слова async (показывают асинхронность метода) и await (ожидание завершения сопрограммы).
Пример. Обращение к трем URL-адресам одновременно с использованием библиотеки Asyncio с использованием async. Метод возвращает сопрограмму и она находится в ожидании.
import asyncio import aiohttp urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org'] async def call_url(url): print('Starting {}'.format(url)) response = await aiohttp.get(url) data = await response.text() print('{}: {} bytes: {}'.format(url, len(data), data)) return data futures = [call_url(url) for url in urls] loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(futures))
Теперь асинхронные приложения Python используют сопрограммы в качестве основного ингредиента. Для их запуска они используют библиотеку Asyncio. Но есть и другие важные элементы, которые также можно считать ключевыми для асинхронных приложений:
Пример. Скрипт парсера с циклами событий и объектами задач в действии.
import asyncio from web_scraping_library import read_from_site_async tasks = [] async def main(url_list): for n in url_list: tasks.append(asyncio.create_task(read_from_site_async(n))) print (tasks) return await asyncio.gather(*tasks) urls = ['http://site1.com','http://othersite.com','http://newsite.com'] loop = asyncio.get_event_loop() results = loop.run_until_complete(main(urls)) print (results)
Метод .get_event_loop() предоставляет объект, позволяющий управлять циклом событий. Все асинхронные функции передаются через .run_until_complete(), который запускает поставленные задачи до тех пор, пока они все не будут выполнены.
Метод .create_task() отдает объект Task для запуска и принимает функцию. Каждый URL-адрес отправляется как отдельный Task в цикл событий. Объекты Task сохраняются в списке. Стоит обратить внимание, что все это можно сделать внутри асинхронной функции или, проще говоря, внутри цикла событий.
Контроль над циклом событий и задачами напрямую зависит от сложности приложения. В примере со скриптом парсера сайта для пристального контроля нет необходимости. Здесь достаточно просто собирать конечные данные, полученные в результате запуска заданий. Все фиксированные задания будут без проблем выполняться здесь одновременно.
Однако если, к примеру, вам необходимо создать фреймворк, уровень контроля над циклом обработки событий и поведением сопрограмм будет значительно выше. Скорее всего, в случае сбоя приложения здесь может потребоваться корректное завершение цикла событий или запуск потоковых задач в безопасном режиме при вызове цикла событий из другого потока.
Для асинхронного программирования в Python помимо зеленых потоков, сопрограмм и обратных вызовов используется мощная библиотека Asyncio — и это на сегодня лучший способ освоить асинхронность в паре с курсами от Hillel. Библиотека доступна для использования с версии Python 3.5. Удобно и то, что она полностью встроена в ядро.
Асинхронный режим в сравнении с многопоточностью имеет ряд преимуществ:
Фактически асинхронность идет рука об руку с многопроцессорностью в Python. Есть возможность использовать asyncio.run_in_executor() для задач, которые интенсивно используют центральный процессор. Также асинхронность решает проблемы потоков:
Есть и небольшие недостатки использования асинхронной библиотеки Asyncio. Во избежание блокировки цикла событий и временных потерь на выполнении асинхронных функций весь код также должен быть асинхронным.
Тем не менее мы можем наблюдать динамику увеличения количества асинхронных библиотек и специального программного обеспечения, предоставляющего асинхронные неблокирующие версии баз данных, сетевых протоколов. Например, репозиторий aio-libs — набор библиотек, созданный на основе Asyncio, в котором представлена асинхронная библиотека aiohttp для веб-доступа. Или же в каталоге пакетов Python также много библиотек с async.
Даже такие монстры как Facebook, Twitter, фреймворк React Native и база данных RocksDB используют асинхронность.
Смог бы, например, Twitter быстро обрабатывать миллиарды сеансов в день без асинхронного программирования?
Так, может, стоит пересмотреть свой код и тоже склониться в сторону асинхронности для обеспечения наибольшей производительности?
Лучший способ научиться асинхронному программированию — посмотреть, как применяют его на практике другие.
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…
Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…