Ответ
В asyncio
, стандартной библиотеке Python для асинхронного программирования, все корутины по умолчанию выполняются в одном потоке — основном потоке цикла событий (event loop).
Почему так:
Корутины — это не потоки операционной системы, а легковесные сопрограммы. Они реализуют кооперативную многозадачность: вместо того чтобы операционная система переключала контекст между потоками, корутины явно "отдают" управление циклу событий, когда встречают оператор await
(например, при ожидании завершения I/O операции). Это позволяет эффективно обрабатывать множество параллельных I/O-bound операций без накладных расходов на создание и переключение между реальными потоками.
Пример:
В данном примере две корутины task("A", 2)
и task("B", 1)
будут выполняться в одном потоке. asyncio.sleep
является асинхронной операцией, которая не блокирует поток, позволяя циклу событий переключиться на другую корутину.
import asyncio
import time
async def task(name, delay):
print(f"[{time.time():.2f}] Корутина '{name}' запущена.")
await asyncio.sleep(delay) # Не блокирует поток, отдает управление event loop
print(f"[{time.time():.2f}] Корутина '{name}' завершена.")
async def main():
print(f"[{time.time():.2f}] Запуск main.")
await asyncio.gather(
task("A", 2),
task("B", 1),
)
print(f"[{time.time():.2f}] Завершение main.")
if __name__ == "__main__":
asyncio.run(main())
Ожидаемый вывод (пример):
[1678886400.00] Запуск main.
[1678886400.00] Корутина 'A' запущена.
[1678886400.00] Корутина 'B' запущена.
[1678886401.00] Корутина 'B' завершена.
[1678886402.00] Корутина 'A' завершена.
[1678886402.00] Завершение main.
Видно, что задачи запускаются почти одновременно и завершаются по мере готовности, не блокируя друг друга.
Когда могут использоваться дополнительные потоки/процессы:
loop.run_in_executor()
: Для выполнения блокирующих или CPU-bound операций (например, сложных вычислений, работы с диском, вызовов синхронных библиотек), которые могли бы заблокировать весь цикл событий.run_in_executor
позволяет вынести такую работу в отдельный поток (используяThreadPoolExecutor
) или процесс (используяProcessPoolExecutor
), не нарушая асинхронность основного цикла.- Низкоуровневые операции: В некоторых случаях, особенно на определенных ОС (например, файловый ввод-вывод в Windows), даже асинхронные операции могут внутренне использовать блокирующие вызовы, которые могут быть обернуты в потоки. Однако это детали реализации, обычно скрытые от пользователя.
Вывод: asyncio
по своей природе однопоточен и предназначен для эффективной обработки I/O-bound задач. Для CPU-bound или других блокирующих операций следует явно использовать run_in_executor
для делегирования их выполнения в отдельные потоки или процессы.