Python Async vs Threads: когда использовать asyncio

Backend2026-09-11TryQuickToolBox

Почему конкурентность важна в Python

У вас есть скрипт на Python, который получает данные из нескольких API. Он работает, но медленно — каждый запрос ждёт завершения предыдущего. Вы слышали о asyncio и потоках, но что использовать? Выбор неправильной модели может привести к коду, который либо излишне сложен, либо мучительно медленный.

Это руководство развеет путаницу. Мы сравним модели async и потоков в Python, покажем, когда именно использовать каждую, и приведём практические примеры, которые вы сможете адаптировать для своих проектов.

Понимание ландшафта конкурентности в Python

Python предлагает три основные модели конкурентности:

GIL — это мьютекс, который предотвращает одновременное выполнение байт-кода Python несколькими потоками. Это означает, что потоки не ускоряют задачи, нагружающие CPU, но они помогают с задачами ввода-вывода, потому что GIL освобождается во время операций ввода-вывода.

Когда использовать asyncio

asyncio блистает, когда ваша программа проводит большую часть времени в ожидании внешних событий: сетевых запросов, файлового ввода-вывода, запросов к базе данных или таймеров. Поскольку он использует один поток, вы избегаете накладных расходов на создание потоков и переключение контекста. Цикл событий эффективно управляет тысячами одновременных соединений.

Используйте asyncio, когда:

Вот простой пример, который получает несколько URL одновременно:

import asyncio
import aiohttp

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

async def main():
    urls = [
        'https://example.com',
        'https://example.org',
        'https://example.net',
    ]
    tasks = [fetch(url) for url in urls]
    results = await asyncio.gather(*tasks)
    print(f'Fetched {len(results)} pages')

asyncio.run(main())

Когда использовать потоки

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

Используйте потоки, когда:

Пример с использованием concurrent.futures.ThreadPoolExecutor:

from concurrent.futures import ThreadPoolExecutor
import requests

def fetch(url):
    response = requests.get(url)
    return response.text

urls = [
    'https://example.com',
    'https://example.org',
    'https://example.net',
]

with ThreadPoolExecutor(max_workers=10) as executor:
    results = list(executor.map(fetch, urls))

print(f'Fetched {len(results)} pages')

Сравнение asyncio и потоков

Вот сравнение бок о бок, которое поможет вам решить:

Аспект asyncio Потоки
Модель конкурентности Однопоточный цикл событий Несколько потоков ОС
Лучше всего для I/O-задачи, высокая конкурентность I/O-задачи, блокирующие вызовы
Производительность на CPU Плохая (GIL) Плохая (GIL)
Масштабируемость Тысячи задач Сотни потоков
Сложность Требует синтаксиса async/await Знакомый синхронный стиль
Отладка Может быть сложной с трассировками Стандартные инструменты отладки
Экосистема Растущая поддержка async Универсальная

А как насчёт задач, нагружающих CPU?

Ни asyncio, ни потоки не помогут вам распараллелить ресурсоёмкие вычисления в Python из-за GIL. Для задач, нагружающих CPU — таких как числовые вычисления, обработка изображений или сжатие данных — используйте multiprocessing или concurrent.futures.ProcessPoolExecutor.

Пример:

from concurrent.futures import ProcessPoolExecutor

def cpu_heavy(n):
    return sum(i * i for i in range(n))

if __name__ == '__main__':
    with ProcessPoolExecutor() as executor:
        results = list(executor.map(cpu_heavy, [10**6, 10**6, 10**6]))
    print(results)

Лучшие практики и подводные камни

FAQ

Могу ли я использовать asyncio и потоки вместе?

Да. Вы можете запускать блокирующий код в пуле потоков с помощью asyncio.to_thread() или loop.run_in_executor(). Это полезно, когда вам нужно вызвать синхронную библиотеку из асинхронного кода, не блокируя цикл событий.

Работает ли asyncio с GIL?

Да, asyncio работает в одном потоке и подчиняется GIL. Однако, поскольку он предназначен для задач ввода-вывода, GIL освобождается во время операций ввода-вывода, позволяя другим задачам выполняться. Для задач, нагружающих CPU, asyncio не даёт ускорения.

Что быстрее: asyncio или потоки?

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

Делаем правильный выбор

Начните с определения, является ли ваше узкое место вводом-выводом или CPU. Если это ввод-вывод и вам нужна высокая конкурентность, выбирайте asyncio. Если это ввод-вывод, но вы имеете дело с блокирующими библиотеками или более простыми скриптами, потоки — надёжный выбор. Для работы, нагружающей CPU, используйте multiprocessing.

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

Когда вам нужно быстро отформатировать или проверить данные JSON, возвращённые из ваших параллельных вызовов API, попробуйте наш JSON Formatter для удобного форматирования и отладки.