Python async/await руководство: базовая теория и практика
Что такое Asynchronous I/O? Что лучше, асинхронность или потоки? Конкурентность и параллелизм, кооперативная многозадачность с примерами
4 min read
Mar 15, 2023
Теория параллельных вычислений
Рассмотрим подходы к оптимизации вычислений: истинная параллельная многозадачность (параллелизм) и псевдопараллельная многозадачность (конкурентное вычисление).
- Параллелизм подразумевает выполнение нескольких операций одновременно. Например, распределение задач по ядрам CPU. Порядок выполнения команд всегда один: control flow детерминирован.
- Конкурентное вычисление предполагает переключение контекста процессора и управление задачами через менеджер, но не истинное одновременное выполнение. Потоки или сопрограммы можно “поставить на паузу” — запускать и останавливать в одном общем процессе. Порядок выполнения команд бывает разный: control flow недетерминирован.
Приоритетная и кооперативная многозадачность
- Вытесняющая многозадачность (приоритетная, preemptive multitasking) — когда переключать контекст решает планировщик: в задачах не указано место переключения, зато указаны таймауты.
Например, многопоточность — это разделение задач по потокам. Ограничение Python Global Interpreter Lock (GIL) позволяет CPython выполнять байткод только одного потока за раз. - Невытесняющая многозадачность (совместная, cooperative multitasking) — когда переключать контекст решает сама задача: в коде явно указывается, когда вернуть выполнение планировщику. Например, асинхронный код переключает контекст оператором await в теле функции.
Проблемы конкурентных вычислений
- Взаимная блокировка (deadlock): несколько потребителей запрашивают один ресурс.
- Ресурсное голодание (starvation): поток с низким приоритетом не успевает получить доступ к ресурсу.
- Состояние гонки (race condition): результат меняется, если поменялся порядок выполнения потоков, например, два потока записывают данные в одну переменную.
- Инверсия приоритетов (priority inversion): поток с высоким приоритетом ждет результат потока с низким приоритетом.
Что такое асинхронность?
- Асинхронное программирование — стиль программирования, когда функция возвращает не сам результат, а обещание результата. Переключение контекста явно указано в теле функции. Следовательно, асинхронный код выполняется конкурентнои кооперативно.
- Async I/O, Asynchronous Input and Output — асинхронный ввод/вывод подразумевает выполнение вычислений во время ожидания результата от ввода и вывода данных.
Плюсы асинхронного программирования
- Легче избежать проблем конкурентных вычислений (блокировка, голодание, гонка, инверсия приоритетов), потому что явно указано переключение контекста.
- Сопрограммы легче масштабировать, чем потоки.
- Асинхронный код выполняется в одном потоке.
Основы Python asyncio
В одном потоке запускается цикл событий Event Loop, он выполняет задачи-футуры Task (базовый класс Future). Футуры создаются из сопрограмм Coroutine и обещают отдать результат в будущем, после ожидания с await.
Футура Task или корутина?
- Coroutine всегда нужен await!
Chain of responsibilities & composite, нужно ждать результат в теле функции. Является возвращаемым значением (результатом вызова) асинхронного генератора: type(coro) == function, type(coro()) == coroutine - Task не нужен await!
Producer-Consumer и фоновые задачи, результат не нужно ждать в теле функции. Представляет будущий результат выполнения корутины, наследует базовый класс Future: asyncio.isfuture(Task()) == True
Методы и объекты Python asyncio
- Event loop:
- asyncio.run(main()) — запуск event loop в Python 3.7+.
- asyncio.get_event_loop().run_until_complete(main()) — получение и запуск event loop в Python 3.7-.
- asyncio.get_event_loop().call_later(timeout, coro) — выполняет указанную корутину спустя указанную задержку.
- asyncio.wait_for(coro, timeout), async with asyncio.timeout(timeout) — указывает ограничение по времени для выполнения задания.
- asyncio.wait(tasks_iter, return_when, timeout) — Блокирует выполнение до достижения указанного условия. Низкоуровневый аналог gather, возвращает два контейнера, с завершенными и незавершенными задача. Для return_when доступны такие значения: FIRST_COMPLETED,FIRST_EXCEPTION, ALL_COMPLETED .
- await asyncio.gather(task(), coro()) — ждет завершения переданных футур и сопрограмм, возвращает одну общую футуру. Высокоуровневый аналог wait.
- Future:
- asyncio.create_task(coro()) — создает футуру Task и добавляет ее в конкурентную очередь задач.
- asyncio.ensure_future(coro()) — создание футуры в Python 3.7-.
- asyncio.Task.cancel() — останавливает футуру Task насильно.
- Queue:
- asyncio.Queue — создает очередь first in, first out.
- asyncio.Queue.get() — возвращает следующий элемент очереди.
- asyncio.Queue.put(x) — добавляет новый элемент в очередь.
- asyncio.Queue.join() — останавливает дальнейшее выполнение тела функции, пока очередь не опустеет.
- asyncio.Queue.task_done() — сообщает очереди, что очередной элемент обработан, и можно переходить к следующему в очереди.
Задача ограниченного буфера или Producer-consumer
Состоит из ограниченного буфера и двух типов сопрограмм: производитель и потребитель. Поставщик добавляет элементы в очередь, а потребитель обрабатывает. Задача:
- Не дать производителю добавить больше элементов, чем может вместить очередь.
- Не дать потребителю запросить элемент из пустой очереди.
Ответ: сценарий медленного потребителя для кооперативной многозадачности, семафоры для вытесняющей и параллелизма.
Сценарий медленного потребителя — потребитель оповещает поставщика, когда взял и обработал данные, а поставщик ждет оповещение. Идеально подходят объекты Task и Queue:
import asyncio, random
async def consumer(buffer):
while True:
val = await buffer.get()
await asyncio.sleep(1)
buffer.task_done()
print('Consumed', val)
async def producer(buffer):
while True:
val = random.randint(1, 100)
await asyncio.sleep(1)
await buffer.put(val)
print('Produced', val)
async def main():
buffer = asyncio.Queue(maxsize=3)
producers = [asyncio.wait_for(
asyncio.create_task(producer(buffer)), 10
) for _ in range(0, 4)]
consumers = [asyncio.create_task(consumer(buffer)) for _ in range(0, 5)]
await asyncio.gather(*producers)
await buffer.join()
for cons in consumers:
cons.cancel()
try:
asyncio.run(main())
except asyncio.TimeoutError:
print('Done')
Python Aiohttp примеры
Aiohttp — асинхронный веб-фреймворк для связи между сервером и клиентом по стандарту REST.
Пишем асинхронный клиент requests на Aiohttp
import aiohttp, asyncio
async def fetch_url(session: aiohttp.ClientSession, url):
response = await session.get(url)
response.raise_for_status()
content_type = response.headers.get('Content-Type')
if 'utf-8' in content_type:
return await response.text()
elif 'json' in content_type:
return await response.json()
else:
return await response.content.read(10)
async def main():
urls = [
'https://docs.aiohttp.org/en/stable/client_reference.html',
'https://jsonplaceholder.typicode.com/todos/1',
'https://placeholder.com/assets/images/150x150-500x500.png'
]
async with aiohttp.ClientSession() as session:
for url in urls:
print(await fetch_url(session, url))
asyncio.run(main())
Пишем сервер RESTful CRUD API на Aiohttp
import aiohttp
from aiohttp import web
class CRUD(web.View):
async def create(request):
data = await request.post()
return web.json_response(dict(data), status=201)
async def get(self):
pk = self.request.match_info.get('pk')
return web.json_response(, status=200)
async def put(self):
pk = self.request.match_info.get('pk')
data = await self.request.json()
return web.json_response(status=202)
async def delete(self):
pk = self.request.match_info.get('pk')
return web.json_response(status=204)
app = web.Application()
app.add_routes([
web.view('/', CRUD), web.post('/', CRUD.create)
])
web.run_app(app)
Спасибо за чтение!
Буду благодарна, если подпишетесь и поделитесь статьей с остальными Python-разработчиками. Давайте вместе развивать наше сообщество.
Асинхронный Python: различные формы конкурентности

Это перевод статьи Абу Ашраф Маснуна «Async Python: The Different Forms of Concurrency».
С появлением Python 3 довольно много шума об «асинхронности» и «параллелизме», можно полагать, что Python недавно представил эти возможности/концепции. Но это не так. Мы много раз использовали эти операции. Кроме того, новички могут подумать, что asyncio является единственным или лучшим способом воссоздать и использовать асинхронные/параллельные операции. В этой статье мы рассмотрим различные способы достижения параллелизма, их преимущества и недостатки.
Определение терминов:
Прежде чем мы углубимся в технические аспекты, важно иметь некоторое базовое понимание терминов, часто используемых в этом контексте.
Синхронный и асинхронный:
В синхронных операциях задачи выполняются друг за другом. В асинхронных — задачи могут запускаться и завершаться независимо друг от друга. Одна асинхронная задача может запускаться и продолжать выполняться, пока выполнение переходит к новой задаче. Асинхронные задачи не блокируют (не заставляют ждать завершения выполнения задачи) операции и обычно выполняются в фоновом режиме.
Например, вы должны обратиться в туристическое агентство, чтобы спланировать свой следующий отпуск. Вам нужно отправить письмо своему руководителю, прежде чем улететь. В синхронном режиме, вы сначала позвоните в туристическое агентство, и если вас попросят подождать, то вы будете ждать, пока вам не ответят. Затем вы начнёте писать письмо руководителю. Таким образом, вы выполняете задачи последовательно, одна за другой.
Но, если вы умны, то пока вас попросили подождать, вы начнёте писать письмо, и когда с вами снова заговорят, вы приостановите написание, поговорите, а затем допишете письмо. Вы также можете попросить друга позвонить в агентство, а сами написать письмо. Это асинхронность, задачи не блокируют друг друга.
Конкурентность и параллелизм:
Конкурентность подразумевает, что две задачи выполняются совместно. В нашем предыдущем примере, когда мы рассматривали асинхронный пример, мы постепенно продвигались то в написании письма, то в разговоре с турагентством. Это конкурентность.
Когда мы попросили позвонить друга, а сами писали письмо, то задачи выполнялись параллельно.
Параллелизм по сути является формой конкурентности. Но параллелизм зависит от оборудования. Например, если в CPU только одно ядро, то две задачи не могут выполняться параллельно. Они просто делят процессорное время между собой. Тогда это конкурентность, но не параллелизм. Но когда у нас есть несколько ядер, мы можем выполнять несколько операций (в зависимости от количества ядер) одновременно.
- Синхронность: блокирует операции (блокирующие)
- Асинхронность: не блокирует операции (неблокирующие)
- Конкурентность: совместный прогресс (совместные)
- Параллелизм: параллельный прогресс (параллельные)
Параллелизм подразумевает конкурентность. Но конкурентность не всегда подразумевает параллелизм.
Потоки и процессы
Python поддерживает потоки уже очень давно. Потоки позволяют выполнять операции конкурентно. Но есть проблема, связанная с Global Interpreter Lock (GIL) из-за которой потоки не могли обеспечить настоящий параллелизм. И тем не менее, с появлением multiprocessing можно использовать несколько ядер с помощью Python.
Потоки (Threads)
Рассмотрим небольшой пример. В нижеследующем коде функция worker будет выполняться в нескольких потоках асинхронно и одновременно.
import threading import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker <>, I slept for <> seconds".format(number, sleep)) for i in range(5): t = threading.Thread(target=worker, args=(i,)) t.start() print("All Threads are queued, let's see when they finish!")
А вот пример выходных данных:
$ python thread_test.py All Threads are queued, let's see when they finish! I am Worker 1, I slept for 1 seconds I am Worker 3, I slept for 4 seconds I am Worker 4, I slept for 5 seconds I am Worker 2, I slept for 7 seconds I am Worker 0, I slept for 9 seconds
Таким образом мы запустили 5 потоков для совместной работы, и после их старта (т.е. после запуска функции worker) операция не ждёт завершения работы потоков прежде чем перейти к следующему оператору print. Это асинхронная операция.
В нашем примере мы передали функцию в конструктор Thread. Если бы мы хотели, то могли бы реализовать подкласс с методом (ООП стиль).
Global Interpreter Lock (GIL)
GIL нужен, чтобы сделать обработку памяти CPython проще и обеспечить наилучшую интеграцию с C.
GIL — это механизм блокировки, когда интерпретатор Python запускает в работу только один поток за раз. Это значит, только один поток может исполняться в байт-коде Python единовременно. GIL следит за тем, чтобы несколько потоков не выполнялись параллельно.
Краткие сведения о GIL:
- Одновременно может выполняться один поток.
- Интерпретатор Python переключается между потоками для достижения конкурентности.
- GIL применим к CPython (стандартной реализации). Но, например, Jython и IronPython не имеют GIL.
- GIL делает однопоточные программы быстрыми.
- Операциям ввода/вывода GIL обычно не мешает.
- GIL позволяет легко интегрировать непотокобезопасные библиотеки на C, благодаря GIL у нас есть много высокопроизводительных расширений/модулей, написанных на C.
- Для CPU-зависимых задач интерпретатор делает проверку каждые N тиков и переключает потоки. Таким образом один поток не блокирует другие.
Многие видят в GIL слабость. Я же рассматриваю это как благо, ведь были созданы такие библиотеки, как NumPy, SciPy, которые занимают особое, уникальное положение в научном обществе.
Процессы (Processes)
Чтобы достичь параллелизма, в Python был добавлен модуль multiprocessing, который предоставляет API и выглядит очень похожим, если вы использовали threading раньше.
Давайте просто пойдем и изменим предыдущий пример. Теперь модифицированная версия использует Процесс вместо Потока.
import multiprocessing import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker <>, I slept for <> seconds".format(number, sleep)) for i in range(5): t = multiprocessing.Process(target=worker, args=(i,)) t.start() print("All Processes are queued, let's see when they finish!")
Что же изменилось? Я просто импортировал модуль multiprocessing вместо threading. А затем, вместо потока я использовал процесс. Вот и всё! Теперь вместо множества потоков мы используем процессы, которые запускаются на разных ядрах CPU (если, конечно, у вашего процессора несколько ядер).
С помощью класса Pool мы также можем распределить выполнение одной функции между несколькими процессами для разных входных значений.
Пример из официальных документов:
from multiprocessing import Pool def f(x): return x*x if __name__ == '__main__': p = Pool(5) print(p.map(f, [1, 2, 3]))
Здесь вместо того, чтобы перебирать список значений и вызывать функцию f по одному, мы фактически запускаем функцию в разных процессах.
Один процесс выполняет f(1), другой-f(2), а другой-f (3). Наконец, результаты снова объединяются в список. Это позволяет нам разбить тяжелые вычисления на более мелкие части и запускать их параллельно для более быстрого расчета.
Модуль concurrent.futures
Модуль concurrent.futures большой и позволяет писать асинхронный код очень легко. Мои любимчики — ThreadPoolExecutor и ProcessPoolExecutor. Эти исполнители поддерживают пул потоков или процессов. Мы отправляем наши задачи в пул, и он запускает задачи в доступном потоке / процессе. Возвращается объект Future, который можно использовать для запроса и получения результата по завершении задачи.
А вот пример ThreadPoolExecutor:
from concurrent.futures import ThreadPoolExecutor from time import sleep def return_after_5_secs(message): sleep(5) return message pool = ThreadPoolExecutor(3) future = pool.submit(return_after_5_secs, ("hello")) print(future.done()) sleep(5) print(future.done()) print(future.result())
Asyncio — что, как и почему
У вас, вероятно, есть вопрос, который есть у многих людей в сообществе Python — что asyncio приносит нового? Зачем нужен был еще один способ асинхронного ввода-вывода? Разве у нас уже не было потоков и процессов?
Зачем нам нужен asyncio?
Процессы очень дорогостоящие и требуют много ресурсов для создания. Поэтому для операций ввода/вывода в основном выбираются потоки.
Мы знаем, что ввод-вывод зависит от внешних вещей — медленные диски или неприятные сетевые лаги делают ввод-вывод часто непредсказуемым. Теперь предположим, что мы используем потоки для операций ввода-вывода. 3 потока выполняют различные задачи ввода-вывода. Интерпретатор должен был бы переключаться между конкурентными потоками и давать каждому из них некоторое время по очереди.
Назовем потоки — T1, T2 и T3. Три потока начали свою операцию ввода-вывода. T3 завершает его первым. T2 и T1 все еще ожидают ввода-вывода. Интерпретатор Python переключается на T1, но он все еще ждет. Хорошо, интерпретатор перемещается в T2, а тот все еще ждет, а затем перемещается в T3, который готов и выполняет код. Вы видите в этом проблему?
T3 был готов, но интерпретатор сначала переключился между T2 и T1 — это понесло расходы на переключение, которых мы могли бы избежать, если бы интерпретатор сначала переключился на T3, верно?
Что есть asynio?
Asyncio предоставляет нам цикл событий наряду с другими крутыми вещами. Цикл событий (event loop) отслеживает события ввода/вывода и переключает задачи, которые готовы и ждут операции ввода/вывода.
Идея очень проста. Есть цикл обработки событий. И у нас есть функции, которые выполняют асинхронные операции ввода-вывода. Мы передаем свои функции циклу событий и просим его запустить их. Цикл событий возвращает нам объект Future, словно обещание, что в будущем мы что-то получим. Мы держимся за обещание, время от времени проверяем, имеет ли оно значение, и, наконец, когда значение получено, мы используем его в некоторых других операциях.
Как использовать asyncio?
Прежде чем мы начнём, давайте взглянем на пример:
import asyncio import datetime import random async def my_sleep_func(): await asyncio.sleep(random.randint(0, 5)) async def display_date(num, loop): end_time = loop.time() + 50.0 while True: print("Loop: <> Time: <>".format(num, datetime.datetime.now())) if (loop.time() + 1.0) >= end_time: break await my_sleep_func() loop = asyncio.get_event_loop() asyncio.ensure_future(display_date(1, loop)) asyncio.ensure_future(display_date(2, loop)) loop.run_forever()
Обратите внимание, что синтаксис async/await предназначен только для Python 3.5 и выше. Пройдёмся по коду:
- У нас есть асинхронная функция display_date, которая принимает число (в качестве идентификатора) и цикл обработки событий в качестве параметров.
- Функция имеет бесконечный цикл, который прерывается через 50 секунд. Но за этот период она неоднократно печатает время и делает паузу. Функция await может ожидать завершения выполнения других асинхронных функций (корутин).
- Передаем функцию в цикл обработки событий (используя метод ensure_future).
- Запускаем цикл событий.
Всякий раз, когда происходит вызов await, asyncio понимает, что функции, вероятно, потребуется некоторое время. Таким образом, он приостанавливает выполнение, начинает мониторинг любого связанного с ним события ввода-вывода и позволяет запускать задачи. Когда asyncio замечает, что приостановленный ввод-вывод функции готов, он возобновляет функцию.
Делаем правильный выбор
Только что мы прошлись по самым популярным формам конкурентности. Но остаётся вопрос — что следует выбрать?
Это зависит от вариантов использования. Из моего опыта я склонен следовать этому псевдо-коду:
if io_bound: if io_very_slow: print("Use Asyncio") else: print("Use Threads") else: print("Multi Processing")
Такие сложные материи, как асинхронность, мы проходим на обучении Рython
Что такое асинхронное программирование в Python

Асинхронное программирование является подходом в разработке программного обеспечения, который позволяет одновременно выполнять несколько задач без блокирования основного потока выполнения. В Python это достигается с помощью асинхронной библиотеки asyncio и ключевых слов async и await .
Преимущества асинхронного программирования
Асинхронное программирование позволяет улучшить производительность приложения, особенно при работе с вводом-выводом (I/O), таким как чтение и запись файлов, обращение к базам данных и веб-сервисам.
Преимущества асинхронного программирования включают:
- Более эффективное использование ресурсов
- Улучшенная отзывчивость приложений
- Упрощение кода для параллельного выполнения задач
Основы асинхронного программирования в Python
Для использования асинхронного программирования в Python, необходимо знакомство с ключевыми словами async и await :
- async используется для объявления асинхронной функции. Это означает, что функция будет возвращать объект coroutine , который можно выполнить асинхронно.
- await используется внутри асинхронной функции для ожидания результата другой асинхронной операции. Это позволяет основному потоку продолжить выполнение других задач, пока ожидается результат.
Пример асинхронного кода на Python:
import asyncio async def my_coroutine(): print("Starting coroutine. ") await asyncio.sleep(1) print("Coroutine finished!") async def main(): await my_coroutine() asyncio.run(main())
Python-разработчик: новая работа через 9 месяцев
Получится, даже если у вас нет опыта в IT

Асинхронное программирование с использованием библиотеки asyncio
В Python стандартная библиотека asyncio предоставляет инструменты для работы с асинхронным программированием, такие как цикл событий (event loop), корутины, футуры (futures) и таски (tasks).
Цикл событий
Цикл событий ( event loop ) является сердцем асинхронного программирования. Он отвечает за запуск, приостановку и возобновление асинхронных задач.
Корутины
Корутины ( coroutines ) представляют собой основные строительные блоки асинхронного кода. Они являются специальными функциями, которые могут приостанавливать свое выполнение, чтобы дать возможность выполнять другие задачи.
Футуры и таски
Футуры ( futures ) и таски ( tasks ) являются объектами, которые представляют результат асинхронных операций. Футуры используются для ожидания результата операции, а таски представляют собой обертку вокруг корутин для выполнения их в цикле событий.
Заключение
Асинхронное программирование в Python позволяет создавать мощные и эффективные приложения, которые могут обрабатывать множество задач одновременно без блокирования основного потока. С помощью ключевых слов async и await , а также библиотеки asyncio , вы сможете создавать асинхронные приложения на Python с отличной производительностью.
Async/await: асинхронные возможности в Python 3+

Иногда у досточтимых джентльменов, обращающих внимание на разнообразие современных технологий асинхронности в Python, возникает вполне закономерный вопрос: «Что, черт возьми, со всем этим делать?» Тут вам и эвентлеты, и гринлеты, и корутины, и даже сам дьявол в ступе (Twisted). Поэтому собрались разработчики, почесали репу и решили: хватит терпеть четырнадцать конкурирующих стандартов, надо объединить их все в один! И как водится, в итоге стандартов стало пятнадцать. Ладно-ладно, шутка :). У событий, описанных в этой статье, конец будет более жизнеутверждающий.
Цикл передач на третьем канале
16 марта 2014 года произошло событие, которое привело к довольно бодрым холиварам, — вышел Python 3.4, а вместе с ним и своя внутренняя реализация event loop’а, которую окрестили asyncio. Идея у этой штуки была ровно такая, как я написал во введении: вместо того чтобы зависеть от внешних сишных реализаций отлова неблокирующих событий на сокетах (у gevent — libevent, у Tornado — IOLoop и так далее), почему бы не встроить одну в сам язык?
Сказано — сделано. Теперь бывалые душители змей вместо того, чтобы в качестве ответа на набивший оскомину вопрос «Что такое корутина?» нырять в генераторы и метод .send() , могли ткнуть в красивый декоратор @asyncio.coroutine и отправить вопрошающего читать документацию по нему.
Правда, сами разработчики отнеслись к новой спецификации довольно неоднозначно и с опаской. Хоть код и старался быть максимально совместимым по синтаксису со второй версией языка — проект tulip, который как раз был первой реализацией PEP 3156 и лег в основу asyncio, был даже в каком-то виде бэкпортирован на устаревшую (да-да, я теперь ее буду называть только так) двойку.
Дело было еще и в том, что реализация, при всей ее красоте и приверженности дзену питона, получилась довольно неторопливая. Разогнанные gevent и Tornado все равно оказывались на многих задачах быстрее. Хотя, раз уж в народ в комьюнити настаивал на тюльпанах, в Tornado таки запилили экспериментальную поддержку asyncio вместо IOLoop, пусть она и была в разы медленнее. Но нашлось у новой реализации и преимущество — стабильность. Пусть соединения обрабатывались дольше, зато ответа в итоге дожидалась бОльшая доля клиентов, чем на многих других прославленных фреймворках. Да и ядро при этом, как ни странно, нагружалось чуть меньше.
Старт был дан, да и какой старт! Проекты на основе нового event loop’а начали возникать, как грибы после дождя, — обвязки для клиентов к базам данных, реализации различных протоколов, тысячи их! Появился даже сайт http://asyncio.org/, который собирал список всех этих проектов. Пусть даже этот сайт не открывался на момент написания статьи из-за ошибки DNS — можешь поверить на слово, там интересно. Надеюсь, он еще поднимется.
Но не все сразу заметили, что над новой версией Python завис великий и ужасный PEP 492.
Сегодня в сопрограмме
Так уж получилось, что довольно большое число людей изначально не до конца поняло смысл введения asyncio и считало его чем-то наподобие gevent, то есть сетевым или даже веб-фреймворком. Но суть у него была совсем другая — он открывал новые возможности асинхронного программирования в ядре языка.
Ты же помнишь в общих чертах, что такое генераторы и корутины (они же сопрограммы)? В контексте Python можно привести два определения генераторов, которые друг друга дополняют:
- Генераторы — это объекты, предоставляющие интерфейс итератора, то есть запоминающие точку последнего останова, которые при каждом обращении к следующему элементу запускают какой-то ленивый код для его вычисления.
- Генераторы — это функции, имеющие несколько точек входа и выхода, заданных с использованием оператора переключения контекста yield.
Корутины же всегда определялись как генераторы, которые, помимо того что вычисляли значения на каждом этапе, могли принимать на каждом обращении параметры, используемые для расчетов следующей итерации. По сути, это и есть вычислительные единицы в контексте того, что называют кооперативной многозадачностью, — можно сделать много таких легковесных корутин, которые будут очень быстро передавать друг другу управление.
В случае сетевого программирования именно это и позволяет нам быстро опрашивать события на сокете, обслуживая тысячи клиентов сразу. Ну или, в общем случае, мы можем написать асинхронный драйвер для любого I/O-устройства, будь то файловая система на block device или, скажем, воткнутая в USB Arduino.
Да, в ядре Python есть пара библиотек, которые изначально предназначались для похожих целей, — это asyncore и asynchat, но они были, по сути, экспериментальной оберткой над сетевыми сокетами, и код для них написан довольно давно. Если ты сейчас, в начале 2017 года, читаешь эту статью — значит, настало время записать их в музейные экспонаты, потому что asyncio лучше.
Давай забудем на время про несвежий Python 2 и взглянем на реализацию простейшего асинхронного эхо-сервера в Python 3.4:
#!/usr/bin/env python import asyncio class EchoProtocol(asyncio.Protocol): def connection_made(self, transport): self.transport = transport print('Connection from <>'.format( transport.get_extra_info('peername') )) def data_received(self, data): message = data.decode() print("Echoing back: ".format(message)) self.transport.write(data) if __name__ == "__main__": loop = asyncio.get_event_loop() server_coro = loop.create_server(EchoProtocol, '127.0.0.1', 7777) server = loop.run_until_complete(server_coro) loop.run_forever()
Нам ничто не мешает подключиться к этому серверу несколькими клиентами и отвечать всем сразу. Это можно проверить, например, с помощью netcat . При этом на сокете будет использоваться лучшая реализация поллинга событий из доступных в системе, в современном Linux это, разумеется, epoll .
Да, этот код асинхронный, но callback hell — тоже вещь довольно неприятная. Немного неудобно описывать асинхронные обработчики как гроздья висящих друг на друге колбэков, не находишь? Отсюда и проистекает тот самый классический вопрос: как же нам, кабанам, писать асинхронный код, который не был бы похож на спагетти, а просто выглядел бы несложно и императивно? На этом месте передай привет в камеру ноутбука (если она у тебя не заклеена по совету ][) тем, кто активно использует Twisted или, скажем, пишет на JavaScript, и поехали дальше.
А теперь давай возьмем Python 3.5 (давно пора) и напишем все на нем.
import asyncio async def handle_tcp_echo(reader, writer): print('Connection from <>'.format( writer.get_extra_info('peername') )) while True: data = await reader.read(100) if data: message = data.decode() print("Echoing back: ".format(message)) writer.write(data) await writer.drain() else: print("Terminating connection") writer.close() break if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete( asyncio.ensure_future( asyncio.start_server(handle_tcp_echo, '127.0.0.1', 7777), loop=loop ) ) loop.run_forever()
Красиво? Никаких классов, просто цикл, в котором мы принимаем подключения и работаем с ними. Если этот код сейчас взорвал тебе мозг, то не волнуйся, мы рассмотрим основы этого подхода.
Для создания подобных серверов и вообще красивой асинхронной работы в Python Дэвид Бизли (обожаю этого парня) написал свою собственную библиотеку под названием curio. Крайне рекомендую ознакомиться, библиотека экспериментальная, но очень приятная. Например, код TCP-сервера на ней может выглядеть так:
from curio import run, spawn from curio.socket import * async def echo_server(address): sock = socket(AF_INET, SOCK_STREAM) sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) sock.bind(address) sock.listen(5) print('Server listening at', address) async with sock: while True: client, addr = await sock.accept() await spawn(echo_client(client, addr)) async def echo_client(client, addr): print('Connection from', addr) async with client: while True: data = await client.recv(100000) if not data: break await client.sendall(data) print('Connection closed') if __name__ == '__main__': run(echo_server(('',25000)))
Несложно заметить, что в случае асинхронного программирования подобным образом в питоне все будет крутиться (каламбур) вокруг того самого внутреннего IOLoop’а, который будет связывать события с их обработчиками. Одной из основных проблем, как я уже говорил, остается скорость — связка Python 2 + gevent, которая использует крайне быстрый libev, по производительности показывает гораздо лучшие результаты.
Но зачем держаться за прошлое? Во-первых, есть curio (см. врезку), а во-вторых, уже есть еще одна, гораздо более скоростная реализация event loop’а, написанная как подключаемый плагин для asyncio, — uvloop, основанный на адски быстром libuv.
Что, уже чувствуешь ураганный ветер из монитора?
Тетя Ася может все
Итак, что же мы имеем? Мы имеем асинхронные функции, они же корутины. Вот такие:
In [1]: async def hello(name): . return "Hello, <>!".format(name)
Если мы просто так возьмем и вызовем эту функцию, ничего не произойдет, потому что нам вернется ленивая корутина. Но мы же помним из статей о генераторах, что нам нужно сделать, чтобы ее запустить? Правильно — передать ей контекст через оператор yield. Формально этого yield’а у нее нет, но мы можем послать в нее значение для того, чтобы «промотать» корутину до следующего переключения контекста:
In [2]: h = hello("Vasya") In [3]: h.send(None) StopIteration Traceback (most recent call last) in () ----> 1 h.send(None) StopIteration: Hello, Vasya!
Что-то знакомое, да? Генератор исчерпался и выкинул StopIteration . Можно, конечно, написать обработчик исключения и дергать корутины через него, но это будет выглядеть крайне странно. Но! Мы можем очень легко вызвать эту корутину из другой корутины!
In [4]: async def call_vasya(): . print(await hello("Vasya"))
Да, мы ее просто «подождем», как маму из той самой песни. Таким образом мы можем выстроить целый разветвленный граф из корутин, которые «ожидают» друг друга и передают управление туда и обратно. Если ты сейчас вскочил с кресла и воскликнул: «Да это же кооперативная многозадачность!» — молодец, к этому все и шло.
Кстати, если все равно назло маме вызвать функцию без await внутри корутины, то нам не просто вернется coroutine object, но еще и в консоль упадет большой warning и напоминание coroutine ‘blablabla’ was never awaited. Ее никто не дождался, поэтому она обиделась и не стала исполняться. Но такие сообщения очень помогают в отладке.
А еще — нельзя просто так взять и вызвать await в интерактивном REPL’е, потому что он не является корутиной сам по себе:
In [5]: await call_vasya() File "", line 1 await call_vasya() ^ SyntaxError: invalid syntax
В остальных случаях await можно писать где угодно внутри корутины, за исключением списковых включений (они же list comprehensions, и это обещают добавить в ближайших релизах) и лямбд (потому что они сами не корутины). А async можно использовать, например, для методов в классе (за исключением «приватных» методов, которые могут дергаться самим Python’ом, понятия не имеющим, что у вас там корутина).
Давай напишем, как нам теперь реально запустить всю эту катавасию:
In [6]: import asyncio In [7]: loop = asyncio.get_event_loop() In [8]: loop.run_until_complete(call_vasya()) Out[8]: 'Hello, Vasya!'
Все довольно просто: мы достаем event loop и заставляем корутину запуститься в нем. Много кода, скажешь? Не особо на самом деле, особенно с учетом того, какие преимущества это нам дает.
Ближе к жизни
Я мог бы рассказать еще про такие штуки, как async for и async with:
- первое — это просто итерация по объекту с ожиданием корутины на каждом шаге, объект должен иметь интерфейсные методы aiter и anext;
- второе — управление контекстом через вызовы корутин — необходимые методы, соответственно, aenter и aexit.
Но лучше почитай про всякую глубинную магию по ссылкам во врезке, а сейчас давай обратимся к более практическому примеру.
![]()
WWW
Есть банальная, казалось бы, задача, которая практически нереализуема во втором питоне, — запустить подпроцесс и асинхронно читать его вывод по мере поступления, как, собственно, и должен работать PIPE.
В последнее время я сильно разочаровался во встроенном модуле subprocess, но к нам на помощь спешит асинхронная реализация, на данный момент часть asyncio. И там это делается просто и красиво.
#!/usr/bin/env python import asyncio import shlex async def _read_stream(stream, cb): """ Асинхронно читаем из потока """ while True: line = await stream.readline() if line: cb(line) else: break async def _stream_subprocess(cmd, stdout_cb, stderr_cb): """ Создаем процесс и делаем два экземпляра корутин, читающих из его stdout и stderr """ process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await asyncio.wait([ _read_stream(process.stdout, stdout_cb), _read_stream(process.stderr, stderr_cb) ]) return await process.wait() def execute(cmd, stdout_cb, stderr_cb): """ Оборачиваем все в event loop """ loop = asyncio.get_event_loop() rc = loop.run_until_complete( _stream_subprocess( cmd, stdout_cb, stderr_cb, ) ) loop.close() return rc if __name__ == '__main__': """ Запускаем команду, передавая ей асинхронные обработчики для stdout и stderr """ cmd = ( "bash -c \"echo stdout && sleep 1 && " "echo stderr 1>&2 && sleep 1 && echo done\"" ) print(execute( shlex.split(cmd), lambda x: print("STDOUT: %s" % x), lambda x: print("STDERR: %s" % x), ))
![]()
WWW
Пример нагло стырен отсюда с небольшими модификациями, потому что он простой и красивый.
Да, это много кода. Но он должен казаться гораздо более понятным после объяснений выше, да и вообще он довольно легко читается. Я, честно сказать, искренне надеюсь, что именно возможности наподобие описанных позволят наконец большему количеству народа распробовать Python 3.5 и перейти на него окончательно.
Сухой остаток
Зачем все это нужно? Затем, что слишком много программ рано или поздно упираются в блокировки — когда мы читаем из сокета, когда мы ждем вывод от процесса, когда мы ждем сигнал от устройства и т. д. и т. п. Обычно такие вещи делаются, например, бесконечным циклом — мы будем стучаться, пока не появятся новые данные нам для обработки, а потом условие выполнится и запустится какой-то код.
Так вот, зачем так делать, если мы можем попросить систему саму отправить нам из kernel space (и опять все дружно скажем «Ave epoll!») сообщение о том, что у нас есть новые данные? Не тратя вычислительные ресурсы на ненужный код.
Я думаю, всем любителям питона стоит исследовать этот новый мир, который нам стал доступен совсем недавно и теперь активно развивается. Нам больше не надо патчить модуль socket через gevent и терпеть адские баги. У нас уже есть готовые асинхронные библиотеки для работы с базами данных (например, aiopg), протоколами (aiozmq), сторонними сервисами через API (aiobotocore) и написания скоростных серверов (aiohttp).
Мало ссылок? Ладно, вот еще одна: реализация протокола HTTP2, которую можно гонять хоть на потоках, хоть на корутинах, — очень интересный проект hyper-h2.
Так чего ты еще тут сидишь? Иди пиши код! Удачи!