Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Asyncio против потоков: выбор

Asyncio против потоков: выбор модели конкурентности

Сравнение asyncio и потоков: модель выполнения, стоимость контекста, обработка блокирующих вызовов и выбор по характеру задач.

Прямой ответ

Обе модели обеспечивают конкурентность при ожидании ввода-вывода, но различаются способом выполнения и стоимостью контекста.

Критерий Asyncio Потоки
Модель выполнения Один поток, переключение по точкам ожидания Несколько потоков, переключение планировщиком
Стоимость контекста Низкая Заметная
Число одновременных задач Тысячи Сотни
Блокирующий вызов Требует выноса в поток Выполняется в потоке
import asyncio

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

async def main():
    return await asyncio.gather(*(fetch(u) for u in urls))

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

Стоимость контекста и блокирующие вызовы

Различие моделей определяет поведение при большом числе одновременных задач и при блокирующих вызовах.

Стоимость контекста. Задача asyncio представляет собой объект с состоянием, поэтому тысячи задач занимают меньше памяти, чем тысячи потоков. Поток требует стека, поэтому число потоков ограничено.

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

Практическое следствие: блокирующий вызов в asyncio выносится в поток, иначе параллелизм теряется.

Отдельный аспект — библиотека. Asyncio требует асинхронной библиотеки для каждой операции, поэтому синхронная библиотека не используется напрямую. Потоки работают с синхронными библиотеками без изменения.

Три признака в пользу потоков.

Синхронная библиотека без асинхронного аналога. Вызов выполняется в потоке, поэтому изменение библиотеки не требуется.

Малое число одновременных задач. Стоимость контекста не критична, поэтому простота потоковой модели перевешивает.

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

Сложность кода и обработка отмены

Выбор модели влияет на структуру кода и способ обработки отмены задач.

Явное ожидание в asyncio. Точка ожидания указывается явно, поэтому места возможного переключения видны в коде. Отсутствие явного ожидания означает, что переключение не происходит.

Неявное переключение в потоках. Переключение выполняется планировщиком, поэтому момент изменения общего состояния не виден в коде. Доступ к общему ресурсу требует блокировки.

Отмена задачи. Задача asyncio отменяется в точке ожидания, поэтому отмена обрабатывается явно. Поток завершается по условию, поэтому принудительная отмена не выполняется.

Практическое следствие: asyncio требует дисциплины в указании точек ожидания, но даёт предсказуемость мест переключения. Потоки не требуют изменения кода, но требуют контроля доступа к общему состоянию.

Распространённая схема сочетает обе модели: асинхронный цикл событий выполняет основную работу, а блокирующие вызовы выносятся в пул потоков. О таймаутах и отмене — в статье про отмену и таймауты asyncio.

Влияние на отладку и наблюдаемость

Модель конкурентности определяет способ трассировки выполнения и сбора диагностических данных.

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

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

Идентификация задачи. Задача asyncio имеет имя, поэтому журнал содержит признак задачи. Поток также имеет имя, поэтому записи различаются по источнику.

Практическое следствие: выбор модели влияет на структуру журнала и трассировки, поэтому способ диагностики определяется заранее.

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

Практика: выбор модели конкурентности

1. Определите характер задач. Ожидание ввода-вывода либо вычисления определяют пригодность модели.

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

3. Проверьте наличие асинхронной библиотеки. Синхронная библиотека без асинхронного аналога выполняется в потоке.

4. Выносите блокирующие вызовы из цикла событий. Вызов без освобождения блокировки останавливает все задачи.

5. Контролируйте доступ к общему состоянию. В потоках переключение неявное, поэтому требуется блокировка.

6. Обрабатывайте отмену явно. Задача asyncio отменяется в точке ожидания, поток завершается по условию.

7. Измеряйте фактическую производительность. Модель выбирается по измерению, а не по предположению.

Рекомендации:

  1. Различайте конкурентность и параллелизм. Обе модели обеспечивают конкурентность при ожидании, но не параллелизм вычислений.
  2. Учитывайте стоимость выноса вызова в поток. Каждый вынос требует передачи данных, поэтому объём влияет на время.
  3. Проверяйте предсказуемость мест переключения. В asyncio точки ожидания явные, в потоках переключение выполняется планировщиком.
  4. Сочетайте модели при необходимости. Асинхронный цикл событий с пулом потоков для блокирующих вызовов.
  5. Пересматривайте выбор при изменении состава задач. Добавление блокирующих вызовов делает asyncio менее пригодным.

Практический сценарий: блокирующий вызов остановил цикл событий

Контекст: асинхронное приложение обрабатывало запросы последовательно, хотя ожидалась конкурентная обработка.

  1. Диагностика. Часть обработчиков выполняла синхронный вызов библиотеки, которая не имела асинхронного аналога. Вызов удерживал управление, поэтому цикл событий не переключался на другие задачи.
  2. Причина. Блокирующий вызов в асинхронном коде останавливает цикл событий, поскольку переключение выполняется только в точках ожидания. Синхронная библиотека не освобождала управление.
  3. Действия. (а) Определили вызовы, которые удерживают управление; (б) вынесли блокирующие вызовы в пул потоков, чтобы цикл событий продолжал работу; (в) заменили часть синхронных библиотек асинхронными аналогами; (г) проверили число одновременно обрабатываемых запросов после изменения; (д) зафиксировали правило: блокирующий вызов не выполняется в цикле событий.
  4. Результат. Запросы обрабатываются конкурентно, поскольку блокирующие вызовы не останавливают цикл событий.

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

FAQ

Какая модель поддерживает больше одновременных задач? Asyncio, поскольку задача представляет собой объект с состоянием и не требует стека потока. Число потоков ограничено объемом памяти на стеки.

Можно ли использовать синхронную библиотеку в asyncio? Напрямую нет, поскольку блокирующий вызов останавливает цикл событий. Вызов выносится в пул потоков.

Нужна ли блокировка в asyncio? Для общего состояния, изменяемого между точками ожидания, требуется синхронизация. В точках ожидания переключение возможно, поэтому состояние может измениться.

Можно ли сочетать обе модели? Да, распространённая схема: асинхронный цикл событий выполняет основную работу, блокирующие вызовы выносятся в пул потоков.

Источники: Python asyncio, threading, concurrent.futures — ссылки в шапке.