Что такое кооперативная многозадачность и как её реализовать с помощью `asyncio`?

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

В asyncio кооперативная многозадачность реализуется через корутины (coroutines). Корутины - это особый вид функций, которые могут приостанавливать свое выполнение и передавать управление другому коду, а затем возобновлять его с того же места. Ключевые слова async и await позволяют определять и использовать корутины.

Пример:


import asyncio

async def task_1():
    print("Задача 1: начало")
    await asyncio.sleep(1)  # Передача управления
    print("Задача 1: конец")

async def task_2():
    print("Задача 2: начало")
    await asyncio.sleep(0.5) # Передача управления
    print("Задача 2: конец")

async def main():
    await asyncio.gather(task_1(), task_2())

asyncio.run(main())
  

В этом примере, asyncio.sleep() приостанавливает выполнение корутины и передает управление event loop'у, который выбирает следующую готовую к выполнению корутину.


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

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

В Python, библиотека asyncio предоставляет инструменты для реализации кооперативной многозадачности. Вот как это можно сделать:

  1. Определение корутин: Корутины – это специальные функции, объявленные с помощью ключевого слова async. Они могут приостанавливать свое выполнение и возобновлять его позже.

    import asyncio
    
    async def my_coroutine(name):
        print(f"Корутина {name}: начало")
        await asyncio.sleep(1)  # Симулируем ожидание
        print(f"Корутина {name}: завершение")
    
  2. Использование await: Ключевое слово await используется для приостановки выполнения корутины до завершения другой асинхронной операции (например, другой корутины или операции ввода-вывода). Когда задача ожидает, она "отдает" управление циклу событий.

  3. Цикл событий (Event Loop): Цикл событий – это сердце asyncio. Он управляет планированием и выполнением корутин. Он следит за готовностью разных задач и переключается между ними, когда одна задача отдает управление.

    async def main():
        task1 = asyncio.create_task(my_coroutine("Первая"))
        task2 = asyncio.create_task(my_coroutine("Вторая"))
    
        await asyncio.gather(task1, task2) # Запускаем корутины конкурентно
    
    if __name__ == "__main__":
        asyncio.run(main())
    

Пример:

import asyncio

async def fetch_data(url):
    print(f"Начинаем загрузку данных с {url}")
    await asyncio.sleep(2)  # Симуляция сетевого запроса
    print(f"Загрузка данных с {url} завершена")
    return f"Данные с {url}"

async def process_data(data):
    print(f"Начинаем обработку данных: {data}")
    await asyncio.sleep(1)  # Симуляция обработки
    print(f"Обработка данных {data} завершена")
    return f"Обработанные данные: {data}"

async def main():
    task1 = asyncio.create_task(fetch_data("https://example.com"))
    task2 = asyncio.create_task(fetch_data("https://google.com"))

    data1 = await task1
    data2 = await task2

    processed_data1 = await process_data(data1)
    processed_data2 = await process_data(data2)

    print(f"Результат 1: {processed_data1}")
    print(f"Результат 2: {processed_data2}")

if __name__ == "__main__":
    asyncio.run(main())

В этом примере, fetch_data и process_data – это корутины. Цикл событий выполняет их конкурентно. Когда fetch_data ожидает завершения "сетевого запроса" (имитируемого с помощью asyncio.sleep), он отдает управление циклу событий, который может переключиться на выполнение fetch_data с другим URL или process_data, если данные уже готовы.

Преимущества использования asyncio:

  • Эффективное использование ресурсов: Позволяет обрабатывать множество одновременных операций ввода-вывода без создания множества потоков или процессов.
  • Улучшенная производительность: Снижает накладные расходы, связанные с переключением контекста, поскольку задачи переключаются добровольно.
  • Более чистый код: Использует ключевые слова async и await для написания асинхронного кода, который выглядит более читаемым и понятным, чем код с обратными вызовами или многопоточностью.

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

0