Пишем нашу первую сопрограмму

Пишем нашу первую сопрограмму

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

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

Асинхронный код сложный, запутанный, требует много обратных вызовов (callback) и обработки ошибок. Чтобы сделать его сделать более читаемым и понятным используют корутины.  Они позволяют писать асинхронный код в синхронном стиле, без необходимости использовать сложные конструкции, такие как промисы (promise) или фьючерсы (future).

Что такое корутины

Корутины или сопрограммы (англ. coroutine) — это специальные функции, которые могут приостанавливать свое выполнение и передавать управление другим корутинам, а затем продолжать с того места, где остановились.

Что могут корутины?

  • Иметь несколько точек входа и выхода: в отличие от обычных подпрограмм, которые имеют одну точку входа и одну точку выхода.
  • Приостанавливать свое выполнение в любой момент: с помощью специального оператора (например, yield в Python или await в Kotlin), сохраняя свое состояние (локальные переменные и стек вызовов).
  • Возобновить свое выполнение с того же места: по запросу другой корутины или внешнего кода.
  • Работать кооперативно: они добровольно отдают управление друг другу, а не конкурируют за ресурсы.
  • Не привязываться к определенному системному потоку (thread), а выполняться поверх них: это означает, что один поток может запускать несколько корутин параллельно, переключаясь между ними по мере необходимости.

Какие языки программирования поддерживают корутины

Корутины не являются новой концепцией в программировании. Они были предложены еще в 1958 году Мелвином Конвеем (Melvin Conway) и использовались в различных языках программирования с тех пор.

Некоторые языки поддерживают корутины на уровне языка, то есть предоставляют специальные ключевые слова или конструкции для работы с ними:

  • Python имеет ключевые слова async и await для объявления и использования корутин.
  • В Kotlin есть ключевое слово suspend для определения функций, которые могут быть вызваны из корутин.
  • В Lua предусмотрены функции coroutine.create и coroutine.resume для создания и запуска корутин.

Другие языки поддерживают корутины на уровне библиотеки, то есть предоставляют специальные классы или функции для работы с ними:

  • C# имеет класс System.Threading.Tasks.Task для представления асинхронных операций, которые могут быть запущены как корутины.
  • Java имеет библиотеку java.util.concurrent.CompletableFuture для создания и комбинирования асинхронных задач.
  • JavaScript имеет объект Promise для оборачивания асинхронных операций в цепочки обратных вызовов.

Код с обратными вызовами VS с корутинами

Код с обратными вызовами

Если мы захотим загрузить данные из сети или из файла, то мы не сможем делать это в основном потоке программы, так как это заблокирует его и сделает наше приложение нереактивным. Вместо этого нам придется запустить этот процесс в фоновом потоке и дождаться его завершения. Минусы такого подхода:

  • Работа с фоновыми потоками сложна и подвержена ошибкам: нужно управлять жизненным циклом потоков, синхронизировать доступ к общим ресурсам и обрабатывать исключения
  • Асинхронный код с обратными вызовами трудно читать и понимать, особенно если он имеет много вложенных вызовов.

Пример кода с callback:

# Асинхронный код с обратными вызовами
def load_data (url, callback):
       # Загрузить данные из url  в фоновом потоке
       # После загрузки вызвать callback c результатом


def process_data (data):
       # Обработать данные
def display_data (data):
       # Отобразить данные на экране


load_data("https://example.com ", lambda data: process_data(data))
display_data(data)

Код с корутинами

Корутины упрощают асинхронный код, позволяя нам писать его в синхронном стиле, как будто мы выполняем обычную последовательность действий:

# Асинхронный код с корутинами
async def load_data(url):
         # Загрузить данные из url  в фоновом потоке
         # Приостановить выполнение корутины до получения результата
         # Вернуть результат


async def process_data(data):
          # Обработать данные


async def display_data(data):
          # Отобразить данные на экране


data = await load_data("https://example.com ")
data = await process_data(data)
await display_data(data)

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

Еще один пример работы сопрограммы

Для того, чтобы создать сопрограмму на Python, нужно определить функцию с ключевым словом async. Это означает, что функция может быть вызвана из другой сопрограммы с помощью оператора await, который приостанавливает выполнение текущей сопрограммы и ждет результата вызванной.

Давайте напишем сопрограмму, которая делает запрос к сайту Bing и возвращает полученный HTML-код:

# Импортируем библиотеку для работы с HTTP-запросами
import aiohttp


# Определяем сопрограмму для запроса к Bing
async def get_bing_html():
         # Создаем асинхронный HTTP-клиент
         async with aiohttp.ClientSession() as session:
         # Делаем GET-запрос к Bing
         async with session.get("https://www.bing.com ") as response:
                  # Читаем ответ как текст
                  html = await response.text()
                  # Возвращаем HTML-код
                  return html

Для того, чтобы запустить эту сопрограмму, нам нужно использовать специальную функцию asyncio.run(), которая принимает сопрограмму в качестве аргумента и запускает ее в цикле событий (event loop).

Цикл событий — это механизм, который управляет выполнением сопрограмм и других асинхронных операций, таких как ввод-вывод или таймеры. Цикл событий постоянно проверяет, есть ли какие-то готовые к выполнению или завершенные задачи, и переключает между ними по мере необходимости.
# Импортируем библиотеку для работы с асинхронностью
import asyncio


# Запускаем сопрограмму для запроса к Bing
bing_html = asyncio.run(get_bing_html())
# Печатаем длину полученного HTML-кода
print(len(bing_html))

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

# Определяем сопрограмму для запроса к Google
async def get_google_html():
         # Создаем асинхронный HTTP-клиент
         async with aiohttp.ClientSession() as session:
                  # Читаем ответ как текст
                  html = await response.text()
                  # Возвращаем HTML-код
                  return html

Теперь мы можем запустить обе сопрограммы параллельно и ждать их результатов:

# Запускаем обе сопрограммы параллельно и ждем их результатов
bing_html, google_html = asyncio.run(asyncio.gather(
        get_bing_html(),
        get_google_html()
))


# Печатаем длины полученных HTML-кодов
print(len(bing_html))
print(len(google_html))

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

Различия между корутинами и потоками

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

  • Потоки — это сущности, которые управляются операционной системой
  • Корутины — сущности, которые управляются языком программирования или библиотекой

  • Потоки имеют свой собственный стек памяти
  • Корутины используют общий стек памяти

  • Потоки переключаются между собой прерывисто (preemptively), то есть операционная система может прервать выполнение одного потока и запустить другой в любой момент
  • Корутины переключаются между собой согласованно (cooperatively), то есть программа или язык определяет точки, в которых корутина может приостановиться и передать управление другой

  • Потоки могут выполняться параллельно на нескольких процессорах или ядрах
  • Корутины выполняются последовательно в рамках одного потока

  • Потоки требуют синхронизации доступа к общим данным с помощью механизмов блокировки, таких как мьютексы (mutex) или семафоры (semaphore)
  • Корутины не требуют блокировки данных, так как они не выполняются одновременно

И наконец — потоки требуют больше ресурсов для создания, уничтожения и переключения, чем корутины.

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

Применение сопрограмм имеет ряд преимуществ по сравнению с использованием потоков. Корутины:

  • Позволяют писать асинхронный код в синхронном стиле, что упрощает чтение и понимание кода.
  • Избавляют от необходимости использовать сложные конструкции, такие как обратные вызовы (callback), промисы (promise) или фьючерсы (future).
  • Экономят память и время, так как они не создают свой собственный стек и не требуют переключения контекста между собой. Это позволяет запускать большое количество корутин на одном потоке без значительных потерь производительности.
  • Поддерживают структурированную параллельность (structured concurrency), то есть они работают в рамках определенной области видимости (scope). Это помогает избежать утечек памяти и ошибок жизненного цикла, так как корутины автоматически отменяются при выходе из своей области видимости.
  • Интегрируются с многими библиотеками и фреймворками для Android, такими как Jetpack, Retrofit и Room. Это позволяет использовать сопрограммы для работы с сетью, базами данных, пользовательским интерфейсом и другими асинхронными операциями.

Недостатки сопрограмм

  • Требуют специального синтаксиса, такого как ключевые слова async и await, для объявления и использования сопрограмм. Такой код не может быть смешан с синхронным кодом без дополнительных преобразований.
  • Не могут выполняться параллельно на нескольких процессорах или ядрах, так как они работают в рамках одного потока. Это означает, что корутины не могут использоваться для задач, которые требуют высокой вычислительной мощности или распределенной обработки.
  • Могут быть сложными для отладки и тестирования, так как они могут приводить к неожиданным результатам из-за асинхронности и кооперативности. Например, корутина может быть отменена в середине выполнения или продолжить выполнение после долгого простоя. Для облегчения отладки и тестирования корутин можно использовать специальные инструменты, такие как Coroutine Debugger или TestCoroutineDispatcher.

В заключение, мы можем сказать, что корутины — это полезный и мощный инструмент для асинхронного программирования, который позволяет писать код в синхронном стиле, экономить ресурсы и поддерживать структурированную параллельность.

Софья Пирогова

Софья Пирогова

Главный редактор / Автор статей
Георгий Бабаян

Георгий Бабаян

Основатель и CEO Эльбрус Буткемп