Что эффективнее для множества сетевых запросов в Python: асинхронность или многопоточность

Ответ

Для обработки большого количества одновременных сетевых запросов (классическая I/O-bound задача) асинхронность (asyncio) является более эффективным и масштабируемым подходом, чем многопоточность (threading).

Почему асинхронность лучше для I/O?

  • Эффективность ресурсов: Асинхронность использует один поток и кооперативную многозадачность. Переключение между задачами (корутинами) происходит только в моменты ожидания ввода-вывода (например, ответа от сервера) и является очень "дешевым" по сравнению с переключением контекста системных потоков, которое требует ресурсов ОС.

  • Масштабируемость: Один поток с asyncio может легко поддерживать тысячи одновременных соединений с минимальными накладными расходами. Создание тысяч системных потоков (threading) было бы крайне затратным для операционной системы и привело бы к деградации производительности.

  • Проблема GIL: Хотя многопоточность хорошо подходит для I/O-bound задач (потоки освобождают GIL во время ожидания), asyncio часто превосходит ее по производительности из-за значительно меньших накладных расходов на переключение контекста.

Пример с asyncio и aiohttp для параллельной загрузки:

import asyncio
import aiohttp
import time

async def fetch_url(session, url):
    # Асинхронно выполняем GET-запрос
    async with session.get(url) as response:
        # Ожидаем статус ответа, не блокируя весь поток
        return response.status

async def main():
    urls = [
        'https://python.org',
        'https://github.com',
        'https://fastapi.tiangolo.com/',
        'https://www.djangoproject.com/',
        'https://flask.palletsprojects.com/'
    ] * 20 # 100 запросов

    async with aiohttp.ClientSession() as session:
        # Создаем список задач (корутин)
        tasks = [fetch_url(session, url) for url in urls]
        # Запускаем все задачи конкурентно и ждем их завершения
        statuses = await asyncio.gather(*tasks)
        print(f"Получено статусов: {len(statuses)}")

if __name__ == "__main__":
    start_time = time.time()
    asyncio.run(main())
    print(f"Время выполнения: {time.time() - start_time:.2f} секунд")

Итог:

  • Асинхронность (asyncio): Лучший выбор для тысяч одновременных I/O-операций (сеть, базы данных). Минимальные накладные расходы.
  • Многопоточность (threading): Хороший вариант для небольшого количества I/O-операций или при работе с блокирующими библиотеками, которые нельзя легко сделать асинхронными.

Ответ 18+ 🔞

Ну ты глянь, какой тут у нас разговор подняли, про асинхронность да многопоточность. Прям как на базаре, только про код. Так вот, слушай сюда, если тебе надо обрабатывать кучу сетевых запросов одновременно — это же классика, I/O-bound задача называется — то забудь ты про эти потоки, как страшный сон. Асинхронность (asyncio) — вот твой единственный и неповторимый выход, ёпта!

А че она такая цаца, эта асинхронность?

  • Ресурсы не жрёт как не в себя: Она в одном потоке работает, задачи между собой по-хорошему, кооперативно, договариваются. Переключиться между ними — раз плюнуть, дешевле пареной репы. А вот эти твои системные потоки — это ж целая история с ОС, переключение контекста — просто пиздец какие накладные расходы, они всю производительность сожрут, как голодные псы.

  • Масштабируется, как сумасшедшая: Один поток, блядь, а держит тысячи соединений — легко! Представь, что будет, если на каждый чих создавать по системному потоку — комп просто накроется медным тазом, удивление пиздец.

  • GIL, ёпта: Да, с потоками для I/O вроде норм (GIL отпускают, пока ждут), но asyncio всё равно обгоняет их на повороте, потому что не тратит силы на ерунду.

Смотри, как это выглядит на практике, с asyncio и aiohttp:

import asyncio
import aiohttp
import time

async def fetch_url(session, url):
    # Делаем запрос асинхронно, не блокируя всю вселенную
    async with session.get(url) as response:
        # Ждём ответ, но другие задачи в это время не спят
        return response.status

async def main():
    urls = [
        'https://python.org',
        'https://github.com',
        'https://fastapi.tiangolo.com/',
        'https://www.djangoproject.com/',
        'https://flask.palletsprojects.com/'
    ] * 20 # Сто запросов, Карл!

    async with aiohttp.ClientSession() as session:
        # Накидаем задач в кучу
        tasks = [fetch_url(session, url) for url in urls]
        # И запустим их всех разом, будто стаю голодных воробьев
        statuses = await asyncio.gather(*tasks)
        print(f"Получено статусов: {len(statuses)}")

if __name__ == "__main__":
    start_time = time.time()
    asyncio.run(main())
    print(f"Время выполнения: {time.time() - start_time:.2f} секунд")

Короче, вывод простой, как три копейки:

  • Асинхронность (asyncio): Твой верный пёс для тысяч операций ввода-вывода. Сеть, базы — всё ей по зубам. Накладных расходов — овердохуища меньше.
  • Многопоточность (threading): Ну, сойдёт для парочки операций или если упёрся в старую библиотеку, которую асинхронной не сделаешь. Но вообще, в рот меня чих-пых, лучше искать асинхронные аналоги.