Asyncio в Python: как ускорить код в 5-10 раз для разработчика
Для кого эта статья:
- 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:
async def my_coroutine():
# Асинхронный код здесь
pass
Когда вы определяете функцию с async def, она не выполняется сразу при вызове. Вместо этого вызов возвращает объект корутины, который нужно передать циклу событий (event loop) для выполнения.
Event loop (цикл событий) — это диспетчер, который управляет выполнением корутин. Он отслеживает все запланированные задачи и выполняет их, переключаясь между ними при необходимости. Вот как выглядит базовое использование цикла событий:
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+) вы можете использовать более простой синтаксис:
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 используется для определения корутин:
# Определение асинхронной функции
async def fetch_data():
# Здесь будет код получения данных
return "Данные получены"
Важно понимать: когда вы вызываете асинхронную функцию, она не выполняется немедленно, а возвращает объект корутины:
# Это НЕ вызовет выполнение функции, а только создаст корутину
coro = fetch_data()
print(coro) # Выведет что-то вроде: <coroutine object fetch_data at 0x...>
Для выполнения корутины необходимо использовать await внутри другой асинхронной функции или передать её циклу событий:
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в генераторах (только в асинхронных генераторах)
Рассмотрим практический пример с имитацией загрузки данных:
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:
- Забывать
awaitперед вызовом асинхронной функции - Пытаться использовать
awaitвне асинхронной функции - Блокировать цикл событий тяжёлыми CPU-операциями
- Смешивать синхронный и асинхронный код неправильным образом
Создание и запуск асинхронных задач с asyncio
Создание отдельных задач (tasks) — ключевой механизм для параллельного выполнения корутин. Tasks — это высокоуровневые абстракции, которые позволяют управлять выполнением корутин и получать их результаты. 🔄
Создать задачу можно несколькими способами:
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():
async def main():
# Запуск нескольких корутин параллельно
results = await asyncio.gather(
process_item("A"),
process_item("B"),
process_item("C")
)
print(f"Все результаты: {results}")
Если вам нужно обработать задачи по мере их завершения (не обязательно в порядке создания), используйте asyncio.as_completed():
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() — возвращает исключение (если оно произошло)
Вот пример отмены задач:
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:
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+):
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-запросов параллельно:
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:
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:
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:
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()для предотвращения зависаний - Профилируйте асинхронный код, чтобы найти узкие места
Пример использования семафора для ограничения параллельных запросов:
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 — инструмент, а не серебряная пуля: используйте его там, где он действительно решает проблемы производительности. Мыслите асинхронно — и ваш код будет работать быстрее, эффективнее и масштабируемее.