Ответ
Да, существуют ситуации, когда многопоточный подход может оказаться производительнее asyncio
для I/O-bound задач.
Основная причина — использование блокирующих библиотек, которые не имеют асинхронных аналогов. asyncio
теряет все свои преимущества, если в event loop попадает блокирующий вызов, так как он останавливает выполнение всех корутин.
Ключевые сценарии, где потоки выигрывают:
-
Интеграция с блокирующим кодом: Если ваше приложение должно работать с библиотекой (например, старый драйвер БД или SDK), которая выполняет блокирующие сетевые или дисковые операции, запуск этих операций в отдельном потоке — единственный способ не блокировать event loop.
-
GIL и I/O: Во время выполнения блокирующей I/O операции (например,
requests.get()
) Python освобождает Global Interpreter Lock (GIL). Это позволяет другим потокам выполняться, обеспечивая реальный параллелизм для I/O-задач. -
Простота для небольших скриптов: Иногда для простого скрипта, делающего несколько десятков запросов, проще и быстрее написать код с использованием
ThreadPoolExecutor
, чем перестраивать его подasync/await
синтаксис.
Практический пример: интеграция блокирующего вызова в asyncio
Современный подход — не выбирать между потоками и asyncio
, а совмещать их. Блокирующий код выполняется в пуле потоков, не мешая основному циклу событий.
import asyncio
import requests
import time
def blocking_io_call(url):
# Эта функция блокирует поток на время запроса
print(f"Начинаю блокирующий запрос к {url}...")
response = requests.get(url)
print(f"Запрос к {url} завершен со статусом {response.status_code}")
return response.status_code
async def main():
loop = asyncio.get_running_loop()
urls = ["https://google.com", "https://github.com"]
start_time = time.time()
# Запускаем блокирующие вызовы в отдельных потоках через executor
# В Python 3.9+ можно использовать asyncio.to_thread()
tasks = [
loop.run_in_executor(None, blocking_io_call, url)
for url in urls
]
results = await asyncio.gather(*tasks)
print(f"Все запросы выполнены за {time.time() - start_time:.2f} секунд.")
print(f"Результаты: {results}")
asyncio.run(main())
Вывод: asyncio
предпочтителен для построения высоконагруженных систем с нативной асинхронной поддержкой. Потоки остаются незаменимым инструментом для работы с блокирующим I/O и могут быть эффективнее в специфических сценариях.