← Назад к вопросам

За счет чего происходит переключение в asyncio?

2.3 Middle🔥 111 комментариев
#Python Core#Асинхронность и многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как происходит переключение задач в asyncio

Переключение в asyncio — это кооперативная многозадачность (cooperative multitasking), которая радикально отличается от предварительного вытеснения потоков. Давайте разберёмся, как это работает.

Основной механизм: event loop и await

import asyncio

async def task1():
    print("task1 start")
    await asyncio.sleep(1)  # Здесь передаём управление
    print("task1 end")

async def task2():
    print("task2 start")
    await asyncio.sleep(0.5)
    print("task2 end")

async def main():
    await asyncio.gather(task1(), task2())

asyncio.run(main())

Вывод:

task1 start
task2 start      # task2 начал, потому что task1 отдал управление
task2 end        # task2 закончился за 0.5 секунд
task1 end        # task1 закончился за 1 сек

Переключение происходит в точках await

Ключевой момент: переключение случается только при await:

  1. Когда функция встречает await, она говорит: "Я чего-то жду, возьми другую задачу"
  2. Event loop берёт следующую готовую задачу
  3. Когда ждущая задача готова (результат приходит), она добавляется обратно в очередь
async def waiting_task():
    print("Начал ждать")
    result = await asyncio.sleep(1)  # ПЕРЕКЛЮЧЕНИЕ! Управление идёт в event loop
    print("Кончил ждать")
    return result

async def working_task():
    for i in range(5):
        print(f"Работаю {i}")
        await asyncio.sleep(0)  # Явное переключение (yield)

Что внутри: event loop и очередь задач

class SimpleEventLoop:
    def __init__(self):
        self.tasks = []  # Очередь готовых задач
        self.waiting = {}  # Ожидающие задачи с timeouts
    
    def run(self, coro):
        task = coro
        while True:
            try:
                awaitable = task.send(None)
                if isinstance(awaitable, asyncio.Future):
                    self.waiting[awaitable] = task
                    ready = self._get_ready_futures()
                    if ready:
                        task = self.waiting.pop(ready[0])
                        continue
                else:
                    break
            except StopIteration:
                break

Переключение vs многопоточность

asyncio (кооперативное):

async def io_bound():
    response = await http_request()
    return response

Многопоточность (вытесняющее):

def io_bound():
    response = requests.get(url)
    return response

Что может прерывать asyncio

Точки переключения:

  • await asyncio.sleep() — явное ожидание
  • await Future / await Task — ожидание асинхронной операции
  • await другая_async_функция() — вызов других async функций
  • IO операции (если используешь асинхронные библиотеки)
async def example():
    await asyncio.sleep(0)
    async with aiohttp.ClientSession() as session:
        data = await session.get(url)
    time.sleep(1)  # ПЛОХО! Блокирует весь loop
    requests.get(url)  # ПЛОХО! Блокирует весь loop

Event loop под капотом

На уровне ОС asyncio использует selectors (epoll на Linux, kqueue на macOS):

import selectors
sel = selectors.DefaultSelector()
while True:
    ready = sel.select(timeout=0)
    for key, mask in ready:
        task.send(None)

Почему это эффективно

  • 1 поток = N задач: одна потокозащита, нет context switching на уровне ОС
  • Масштабируемость: легко обработать 10k одновременных соединений
  • Детерминированность: точно знаешь, где переключения, нет race conditions
async def main():
    tasks = [fetch_data(i) for i in range(10000)]
    results = await asyncio.gather(*tasks)

Важные выводы

  1. Переключение — явное: только в точках await
  2. Не блокирующие операции: всегда используй асинхронные версии (aiohttp, asyncpg)
  3. Event loop — синглтон: весь код в одном потоке, нет гонок
  4. Масштабируемость: идеально для I/O bound задач