Asyncio в Python: как ускорить код в 5-10 раз для разработчика

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Python-разработчики, стремящиеся усовершенствовать свои навыки
  • Начинающие программисты, интересующиеся асинхронным программированием
  • Специалисты, работающие с высоконагруженными приложениями и I/O-операциями

    Асинхронное программирование в Python изменило правила игры. Если вы устали от медлительности синхронного кода при работе с I/O-операциями или API-запросами, asyncio станет вашим секретным оружием. Вместо того чтобы бездействовать в ожидании ответа сервера, ваш код может параллельно выполнять другие задачи, значительно ускоряя работу приложения. Это не просто модный тренд — это необходимость для разработчиков, стремящихся создавать высокопроизводительные приложения. 🚀

Хотите быстро освоить asyncio и стать востребованным Python-разработчиком? Обучение Python-разработке от Skypro предлагает углубленный модуль по асинхронному программированию под руководством практикующих разработчиков. Вы не просто изучите теорию, а сразу примените asyncio в реальных проектах. Наши выпускники создают высоконагруженные приложения, которые работают в 5-10 раз быстрее обычных. Присоединяйтесь и станьте экспертом в асинхронном программировании!

Что такое asyncio и зачем он нужен в Python

Модуль asyncio — это библиотека для написания конкурентного кода с использованием синтаксиса async/await. Он был добавлен в стандартную библиотеку Python начиная с версии 3.4 и полностью раскрыл свой потенциал в Python 3.7+.

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

Михаил Соколов, Senior Python Developer Я руководил проектом парсера данных, который ежедневно обрабатывал информацию с 50+ источников. Изначально код был синхронным — каждый запрос блокировал выполнение следующего. Процесс занимал около 4 часов. Когда бизнес потребовал ускорить обработку, мы переписали парсер с использованием asyncio.

Результат превзошёл ожидания: время выполнения сократилось до 20 минут! Это произошло потому, что парсер стал отправлять запросы параллельно и обрабатывать ответы по мере их поступления, вместо ожидания каждого по отдельности.

Самым сложным оказался не сам переход на asyncio, а изменение мышления команды. Асинхронный код требует иного подхода к структурированию программы.

Основные причины использования asyncio:

  • Повышение производительности — asyncio позволяет обрабатывать тысячи подключений на одном потоке
  • Эффективная работа с I/O-операциями — вместо простаивания при ожидании ввода/вывода, программа выполняет другие задачи
  • Более чистый код — по сравнению с многопоточностью, asyncio обычно приводит к более понятному и поддерживаемому коду
  • Избегание проблем с блокировками — отсутствие необходимости в механизмах синхронизации, характерных для многопоточности
Характеристика Синхронный код Код с asyncio
Выполнение задач Последовательное Конкурентное
Эффективность при I/O-операциях Низкая (простаивание) Высокая (выполнение других задач)
Сложность кода Низкая Средняя
Масштабируемость Ограниченная Высокая

Важно понимать: asyncio не является многопоточным или многопроцессорным решением. Весь код выполняется в одном потоке, но организован так, чтобы использовать время ожидания для других задач. Это делает asyncio идеальным для I/O-bound задач, но не для CPU-bound операций (для них лучше использовать многопроцессорность).

Пошаговый план для смены профессии

Основы работы с корутинами и event loop в asyncio

Корутины (coroutines) — это функции, которые могут приостанавливать своё выполнение, позволяя другому коду выполняться, и затем возобновлять работу с того места, где они остановились. Это краеугольный камень асинхронного программирования в Python.

Создание корутины осуществляется с помощью ключевого слова async:

Python
Скопировать код
async def my_coroutine():
# Асинхронный код здесь
pass

Когда вы определяете функцию с async def, она не выполняется сразу при вызове. Вместо этого вызов возвращает объект корутины, который нужно передать циклу событий (event loop) для выполнения.

Event loop (цикл событий) — это диспетчер, который управляет выполнением корутин. Он отслеживает все запланированные задачи и выполняет их, переключаясь между ними при необходимости. Вот как выглядит базовое использование цикла событий:

Python
Скопировать код
import asyncio

async def main():
# Асинхронные операции
await asyncio.sleep(1)
print("Привет, асинхронный мир!")

# Получаем цикл событий
loop = asyncio.get_event_loop()
# Запускаем корутину в цикле событий до её завершения
loop.run_until_complete(main())
# Закрываем цикл событий
loop.close()

В современных версиях Python (3.7+) вы можете использовать более простой синтаксис:

Python
Скопировать код
import asyncio

async def main():
await asyncio.sleep(1)
print("Привет, асинхронный мир!")

# Запускает корутину, автоматически управляя циклом событий
asyncio.run(main())

Ключевые понятия при работе с event loop:

  • Tasks (задачи) — обёртки вокруг корутин, которые отслеживают их выполнение
  • Futures — объекты, представляющие результат операции, которая ещё не завершена
  • await — ключевое слово для приостановки корутины до завершения другой корутины
  • asyncio.gather() — функция для параллельного запуска нескольких корутин

Когда корутина выполняет await, она приостанавливается, позволяя циклу событий выполнять другие задачи. Когда ожидаемая операция завершается, цикл событий возвращается к приостановленной корутине и продолжает её выполнение.

Функция event loop Назначение Пример использования
rununtilcomplete() Запускает корутину до её завершения loop.run_until_complete(coro)
create_task() Создаёт Task из корутины task = loop.create_task(coro)
run_forever() Запускает цикл событий бесконечно loop.run_forever()
stop() Останавливает цикл событий loop.stop()
close() Закрывает цикл событий loop.close()

Синтаксис async/await: первые шаги для новичков

Синтаксис async/await — это элегантный способ выразить асинхронность в Python. Давайте разберём его основы на простых примерах. 🧩

Ключевое слово async используется для определения корутин:

Python
Скопировать код
# Определение асинхронной функции
async def fetch_data():
# Здесь будет код получения данных
return "Данные получены"

Важно понимать: когда вы вызываете асинхронную функцию, она не выполняется немедленно, а возвращает объект корутины:

Python
Скопировать код
# Это НЕ вызовет выполнение функции, а только создаст корутину
coro = fetch_data()
print(coro) # Выведет что-то вроде: <coroutine object fetch_data at 0x...>

Для выполнения корутины необходимо использовать await внутри другой асинхронной функции или передать её циклу событий:

Python
Скопировать код
async def main():
# await приостанавливает выполнение main до завершения fetch_data
result = await fetch_data()
print(result) # "Данные получены"

# Запуск корутины через цикл событий
import asyncio
asyncio.run(main())

Ключевое слово await может применяться только внутри функций, определённых с async def. Оно приостанавливает выполнение текущей корутины до завершения указанной операции.

Вот несколько важных правил при работе с async/await:

  • await можно использовать только внутри async def функций
  • С помощью await можно ожидать только "awaitable" объекты: корутины, задачи и футуры
  • Асинхронные функции не могут использовать return с await в одном выражении
  • Нельзя использовать await в генераторах (только в асинхронных генераторах)

Рассмотрим практический пример с имитацией загрузки данных:

Python
Скопировать код
import asyncio

async def fetch_user(user_id):
print(f"Начинаем загрузку пользователя {user_id}...")
# Имитация сетевой задержки
await asyncio.sleep(2)
print(f"Пользователь {user_id} загружен")
return {"id": user_id, "name": f"User {user_id}"}

async def main():
user = await fetch_user(42)
print(f"Получены данные: {user}")

asyncio.run(main())

Выполнение этого кода даст такой результат:

Начинаем загрузку пользователя 42...
Пользователь 42 загружен
Получены данные: {'id': 42, 'name': 'User 42'}

Алексей Петров, Python Backend Developer Помню свой первый проект с asyncio — сервис для агрегации данных с различных API. Раньше запросы отправлялись последовательно, и пользователям приходилось ждать по 15-20 секунд для получения результатов.

Решил переписать код с использованием async/await. Первые дни были мучительными — я постоянно путался, где ставить await, почему код не выполняется как ожидалось, и почему результаты приходят в неправильном порядке.

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

Конечный результат: время ответа сократилось до 2-3 секунд, так как запросы выполнялись параллельно. Пользователи были в восторге, а я осознал мощь асинхронного программирования.

Типичные ошибки новичков при работе с async/await:

  1. Забывать await перед вызовом асинхронной функции
  2. Пытаться использовать await вне асинхронной функции
  3. Блокировать цикл событий тяжёлыми CPU-операциями
  4. Смешивать синхронный и асинхронный код неправильным образом

Создание и запуск асинхронных задач с asyncio

Создание отдельных задач (tasks) — ключевой механизм для параллельного выполнения корутин. Tasks — это высокоуровневые абстракции, которые позволяют управлять выполнением корутин и получать их результаты. 🔄

Создать задачу можно несколькими способами:

Python
Скопировать код
import asyncio

async def process_item(item):
print(f"Обрабатываем {item}")
await asyncio.sleep(1) # Имитация обработки
print(f"{item} обработан")
return f"Результат {item}"

async def main():
# Способ 1: create_task
task1 = asyncio.create_task(process_item("A"))

# Способ 2: ensure_future (устаревший метод)
task2 = asyncio.ensure_future(process_item("B"))

# Ожидаем завершения задач
result1 = await task1
result2 = await task2

print(f"Результаты: {result1}, {result2}")

asyncio.run(main())

Функция create_task() планирует выполнение корутины и возвращает объект Task, который можно затем ожидать с помощью await.

Для параллельного выполнения нескольких задач и ожидания их всех, используйте asyncio.gather():

Python
Скопировать код
async def main():
# Запуск нескольких корутин параллельно
results = await asyncio.gather(
process_item("A"),
process_item("B"),
process_item("C")
)

print(f"Все результаты: {results}")

Если вам нужно обработать задачи по мере их завершения (не обязательно в порядке создания), используйте asyncio.as_completed():

Python
Скопировать код
async def main():
tasks = [
process_item("A"), # Займет 1 секунду
process_item("B"), # Займет 1 секунду
process_item("C") # Займет 1 секунду
]

# Получение результатов по мере готовности
for future in asyncio.as_completed(tasks):
result = await future
print(f"Задача завершена с результатом: {result}")

Для управления выполнением задач предусмотрены различные методы:

  • cancel() — отменяет выполнение задачи
  • done() — проверяет, завершилась ли задача
  • result() — возвращает результат (если задача завершена)
  • exception() — возвращает исключение (если оно произошло)

Вот пример отмены задач:

Python
Скопировать код
async def main():
task = asyncio.create_task(process_item("X"))

# Даём задаче немного поработать
await asyncio.sleep(0.5)

# Отменяем задачу до её завершения
task.cancel()

try:
await task
except asyncio.CancelledError:
print("Задача была отменена")

Обработка исключений в асинхронных задачах требует особого внимания. Когда исключение возникает внутри задачи, оно не будет автоматически распространяться, если вы не используете await:

Python
Скопировать код
async def risky_operation():
await asyncio.sleep(1)
raise ValueError("Что-то пошло не так")

async def main():
# Вариант 1: Исключение будет поймано
try:
await risky_operation()
except ValueError as e:
print(f"Поймано исключение: {e}")

# Вариант 2: Задача с исключением
task = asyncio.create_task(risky_operation())

# Ожидаем завершения, исключение будет проброшено
try:
await task
except ValueError as e:
print(f"Поймано исключение из задачи: {e}")

Для работы с группами задач можно использовать контекстный менеджер asyncio.TaskGroup (Python 3.11+):

Python
Скопировать код
async def main():
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(process_item("A"))
task2 = tg.create_task(process_item("B"))
task3 = tg.create_task(process_item("C"))

# После выхода из контекста все задачи будут завершены
print(f"Результаты: {task1.result()}, {task2.result()}, {task3.result()}")

Практическое применение asyncio в реальных проектах

Теоретические знания об asyncio становятся по-настоящему ценными, когда вы начинаете применять их в практических сценариях. Рассмотрим несколько типичных случаев использования. ⚙️

1. Асинхронные HTTP-запросы с aiohttp

Одно из самых распространённых применений asyncio — выполнение множества HTTP-запросов параллельно:

Python
Скопировать код
import asyncio
import aiohttp
import time

async def fetch(session, url):
async with session.get(url) as response:
return await response.text()

async def main():
urls = [
'https://example.com',
'https://python.org',
'https://docs.python.org',
'https://github.com',
'https://stackoverflow.com'
]

start_time = time.time()

async with aiohttp.ClientSession() as session:
# Запускаем все запросы параллельно
tasks = [fetch(session, url) for url in urls]
results = await asyncio.gather(*tasks)

for url, html in zip(urls, results):
print(f"Получено {len(html)} байт с {url}")

print(f"Все запросы выполнены за {time.time() – start_time:.2f} секунд")

asyncio.run(main())

2. Асинхронная работа с базами данных

Для работы с БД используйте асинхронные драйверы, например asyncpg для PostgreSQL:

Python
Скопировать код
import asyncio
import asyncpg

async def fetch_users(db_pool):
async with db_pool.acquire() as connection:
return await connection.fetch('SELECT * FROM users LIMIT 100')

async def update_user_status(db_pool, user_id, status):
async with db_pool.acquire() as connection:
await connection.execute(
'UPDATE users SET status = $1 WHERE id = $2',
status, user_id
)

async def main():
# Создаём пул соединений с базой данных
pool = await asyncpg.create_pool(
user='username',
password='password',
database='dbname',
host='127.0.0.1'
)

# Получаем пользователей и обновляем их статусы параллельно
users = await fetch_users(pool)

# Создаём задачи для обновления
tasks = [
update_user_status(pool, user['id'], 'active') 
for user in users
]

# Выполняем все обновления параллельно
await asyncio.gather(*tasks)

await pool.close()

# asyncio.run(main()) # Раскомментировать при запуске

3. Асинхронная обработка файлов

Для асинхронной работы с файловой системой используйте модуль aiofiles:

Python
Скопировать код
import asyncio
import aiofiles

async def process_large_file(filename):
async with aiofiles.open(filename, mode='r') as file:
contents = await file.read()

# Обработка содержимого файла
processed_data = contents.upper()

# Запись результата в новый файл
async with aiofiles.open(f'processed_{filename}', mode='w') as output_file:
await output_file.write(processed_data)

return f'Файл {filename} обработан'

async def main():
files = ['file1.txt', 'file2.txt', 'file3.txt']
tasks = [process_large_file(file) for file in files]
results = await asyncio.gather(*tasks)

for result in results:
print(result)

# asyncio.run(main()) # Раскомментировать при запуске

4. Реализация асинхронного веб-сервера

С помощью asyncio можно создать высокопроизводительный веб-сервер, например с aiohttp:

Python
Скопировать код
from aiohttp import web
import asyncio

async def handle(request):
# Имитация обработки данных
await asyncio.sleep(0.1)
return web.json_response({"status": "success", "data": "Запрос обработан"})

async def main():
app = web.Application()
app.router.add_get('/', handle)
app.router.add_get('/api/data', handle)

return app

# Запуск сервера
# web.run_app(main())

Сценарий использования Библиотека Преимущества
HTTP-клиент aiohttp Параллельные запросы, управление сессиями, потоковая передача
Работа с PostgreSQL asyncpg Высокая производительность, поддержка пула соединений
Работа с Redis aioredis Асинхронные операции с кэшем, pub/sub
Работа с файлами aiofiles Неблокирующие операции ввода/вывода
Веб-фреймворки FastAPI, Sanic Высокая производительность, асинхронные обработчики

5. Советы по оптимизации асинхронного кода

  • Избегайте блокирующих операций в асинхронных функциях — они блокируют весь цикл событий
  • Используйте asyncio.gather() для параллельного выполнения задач
  • Контролируйте количество одновременных задач с помощью семафоров (asyncio.Semaphore)
  • Применяйте тайм-ауты с asyncio.wait_for() для предотвращения зависаний
  • Профилируйте асинхронный код, чтобы найти узкие места

Пример использования семафора для ограничения параллельных запросов:

Python
Скопировать код
async def main():
# Ограничиваем количество одновременных запросов до 10
semaphore = asyncio.Semaphore(10)

async def fetch_with_limit(url):
async with semaphore: # Захватываем семафор
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()

urls = [f"https://example.com/{i}" for i in range(100)]
tasks = [fetch_with_limit(url) for url in urls]

# Выполняем задачи с ограничением параллельности
await asyncio.gather(*tasks)

Путь к мастерству в асинхронном программировании — это непрерывная практика и эксперименты. Начните с простых примеров, постепенно внедряя asyncio в свои проекты. Преимущества асинхронности наиболее заметны в I/O-интенсивных приложениях: веб-краулерах, API-серверах, микросервисах. Помните, что asyncio — инструмент, а не серебряная пуля: используйте его там, где он действительно решает проблемы производительности. Мыслите асинхронно — и ваш код будет работать быстрее, эффективнее и масштабируемее.

Загрузка...