Асинхронность в Python: как Twitter обрабатывает миллиарды сеансов в день

Ольга Змерзла

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

Асинхронность — это возможность выполнения программой задач и процессов без ожидания их завершения.

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

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

Асинхронное программирование может быть полезным, если:

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

Как уже было сказано выше, не всегда поочередное выполнение строк кода бывает эффективным. Проблему последовательности могут решить так называемые потоки (threads). Благодаря им программа может выполнять несколько задач одновременно. 

Многопоточные программы представляют собой более сложную структуру, а потому больше подвержены ошибкам и сбоям. Из наиболее распространенных — ресурсное голодание, взаимная блокировка, состояние гонки. Чтобы не допускать ошибок можно пойти на курс от Mate Academy и выучить все тонкости с практикующими специалистами.

Переключения контекста процессора

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

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

Зеленые потоки (green threads)

Зеленые потоки позволяют очень быстро писать асинхронный код. Особенность их использования заключается в том, что переключения между 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): 

  • Сопрограммы запускаются в цикле событий (event loop).
  • Цикл событий выполняется в потоке, и происходит получение задач из очереди.
  • Каждая из задач вызывает следующий шаг сопрограммы.
  • Если сопрограмма вызывает другую сопрограмму, текущая сопрограмма приостанавливается и происходит переключение контекста. При этом контекст текущей сопрограммы сохраняется.
  • Если сопрограмма встречает код блокировки, текущая сопрограмма приостанавливается, а управление передается обратно в цикл обработки событий.
  • Цикл событий получает следующие задачи из очереди.
  • Затем цикл событий возвращается к первой задаче с того места, где она была приостановлена.

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

Обратный вызов (callback)

Помимо распространенных библиотек для асинхронного программирования в 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()

где:

  • handle_response — обратный вызов;
  • print(“Error:”, response.error) — строка проверяет ошибки. Это необходимо, потому что из-за цикла событий код не может обрабатывать исключения. Любые исключения, которые могут быть созданы в функции обратного вызова, являются причиной остановки программы и цикла событий. Именно поэтому они передаются в виде объектов. Если данная строка не будет проверять ошибки, они не будут обрабатываться вообще.

Благодаря используемому в коде методу AsyncHTTPClient.fetch информацию об URL-адресе можно получать без блокировки. Каждая последующая строка выполняется еще до того, как получен ответ по URL, а значит, результат выполнения получить невозможно. fetch возвращает объект, вызывая функцию обратного вызова — handle_response.

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

Генераторы в Python

Генераторы позволяют обрабатывать огромные потоки данных. Представим, что у вас есть файл немыслимых размеров и вам необходимо обработать и вычленить необходимую информацию. Маловероятно, что локально у вас будет достаточно места и памяти на личном ПК для обработки огромного объема данных, если только вы не станете делать это частями. В 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))

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

Async и await

Благодаря своей универсальности и мощности библиотека 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 и их компоненты

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

  • Циклы событий (Event loops). Asyncio создает циклы событий и управляет ими. Циклы запускают сопрограммы еще до их завершения. Для удобства отслеживания процесса одновременно может выполняться только один цикл обработки событий.
  • Задачи (Tasks). При запуске сопрограммы в цикле событий, есть возможность вернуть объект Task. Он управляет поведением сопрограммы вне цикла событий. Хотите отменить запущенную задачу? Вызовите метод .cancel().

Пример. Скрипт парсера с циклами событий и объектами задач в действии.

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. Удобно и то, что она полностью встроена в ядро.

Асинхронный режим в сравнении с многопоточностью имеет ряд преимуществ:

  • Функции асинхронности намного легче потоков. Это значит, что сотни и тысячи асинхронных операций, выполняемых одновременно, будут расходовать гораздо меньше ресурсов, чем сотни и тысячи потоков.
  • По сравнению с потоками асинхронными операциями намного легче управлять. Например, их можно отменять, задействовав объект Taskasyncio.create_task().
  • В цикле асинхронных событий задачи выполняются в одном потоке, их проще понимать, контролировать и отслеживать.

Фактически асинхронность идет рука об руку с многопроцессорностью в Python. Есть возможность использовать asyncio.run_in_executor() для задач, которые интенсивно используют центральный процессор. Также асинхронность решает проблемы потоков:

  1. Библиотека Asyncio осуществляет переключение контекста на уровне программы, а не процессора. Используется цикл событий.
  2. Отсутствие состояние гонки. Поскольку с Asyncio переключение контекста осуществляется в заранее созданных точках, код не испытывает проблему гонки. Запускается одна сопрограмма, которая переключается только в заданных вами точках.
  3. Гораздо меньшее ресурсное голодание. Конечно, в Asyncio имеется пул потоков, который может влиять на расходование ресурсов (запуск большого количества процессов). Но тем не менее запуск сопрограмм в одном потоке задействует гораздо меньше памяти на выполнение процессов.
  4. Взаимная блокировка. Отсутствие гонки потоков практически сводит к нулю различного рода блокировки.

Есть и небольшие недостатки использования асинхронной библиотеки Asyncio. Во избежание блокировки цикла событий и временных потерь на выполнении асинхронных функций весь код также должен быть асинхронным.

Тем не менее мы можем наблюдать динамику увеличения количества асинхронных библиотек и специального программного обеспечения, предоставляющего асинхронные неблокирующие версии баз данных, сетевых протоколов. Например, репозиторий aio-libs — набор библиотек, созданный на основе Asyncio, в котором представлена асинхронная библиотека aiohttp для веб-доступа. Или же в каталоге пакетов Python также много библиотек с async. 

Даже такие монстры как Facebook, Twitter, фреймворк React Native и база данных RocksDB используют асинхронность.

Смог бы, например, Twitter быстро обрабатывать миллиарды сеансов в день без асинхронного программирования?

Так, может, стоит пересмотреть свой код и тоже склониться в сторону асинхронности для обеспечения наибольшей производительности?

Лучший способ научиться асинхронному программированию — посмотреть, как применяют его на практике другие.

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023