Корутина¶
Корутина — это компонент компьютерной программы, обобщающий понятие подпрограммы (функции) и позволяющий приостанавливать своё выполнение в определённых точках, сохраняя состояние (локальные переменные, счётчик команд), и возобновлять выполнение с места остановки. В отличие от обычных функций, которые выполняются строго от начала до конца (режим «вызов-возврат»), корутины могут иметь несколько точек входа и выхода, что делает их удобным инструментом для реализации конкурентности, асинхронного ввода-вывода, конечных автоматов и кооперативной многозадачности.
¶История
Понятие корутины впервые было формализовано в 1963 году Мелвином Конвеем в статье «Design of a Separable Transition-diagram Compiler». Конвей использовал корутины для организации взаимодействия между компонентами компилятора, где каждая фаза компиляции (лексический анализ, синтаксический анализ, генерация кода) могла работать как корутина, передавая управление следующей фазе. Термин «coroutine» (от англ. cooperative routine — кооперативная подпрограмма) был введён именно в этой работе.
В 1970-х годах корутины были реализованы в языке Simula 67, где они использовались для моделирования дискретных событий. В 1975 году корутины появились в языке Modula-2. Однако широкое распространение концепция получила лишь в начале 2000-х годов с развитием языков, поддерживающих асинхронное программирование. В 2002 году корутины были добавлены в язык Python (в виде генераторов), а в 2012 году — в C# (ключевые слова async/await). В 2015 году корутины стали частью стандарта ECMAScript (JavaScript). В 2017 году корутины появились в C++20 (стандарт C++20).
¶Отличия от подпрограмм и потоков
¶От подпрограмм (функций)
- Управление: Обычная функция после вызова выполняется до конца (или до оператора
return), после чего управление возвращается в вызывающий код. Корутина может приостанавливать выполнение в произвольной точке (с помощью ключевого словаyieldилиawait) и передавать управление обратно вызывающему коду, а затем возобновляться с того же места. - Состояние: Корутина сохраняет своё внутреннее состояние (локальные переменные, стек вызовов) между приостановками. Функция при каждом вызове создаёт новое состояние.
- Стек: Корутины могут быть стековыми (имеют собственный стек вызовов) или бесстековыми (используют стек вызывающего кода). Функции всегда используют стек вызывающего кода.
¶От потоков (threads)
- Вытеснение: Потоки операционной системы являются вытесняющими — планировщик ОС может прервать выполнение потока в любой момент, переключившись на другой поток. Корутины являются кооперативными — они передают управление добровольно, в явно указанных точках приостановки.
- Накладные расходы: Переключение между потоками требует системных вызовов и переключения контекста ядра, что относительно дорого. Переключение между корутинами — это операция уровня пользователя, выполняемая за счёт сохранения и восстановления нескольких регистров, что значительно дешевле.
- Параллелизм: Потоки могут выполняться параллельно на разных ядрах процессора. Корутины выполняются в одном потоке (хотя могут быть распределены по потокам с помощью планировщика) и обеспечивают конкурентность, а не параллелизм.
¶Виды корутин
¶По способу управления состоянием
- Бесстековые корутины (stackless coroutines): Не имеют собственного стека вызовов. Всё состояние хранится в статической или динамической памяти. Приостановка возможна только в самой корутине, но не во вложенных вызовах. Примеры: генераторы Python, корутины C++20,
async/awaitв C#. - Стековые корутины (stackful coroutines): Имеют собственный стек, что позволяет приостанавливать выполнение из любой вложенной функции. Примеры: корутины в Lua, Goroutines в Go (хотя Go использует модель, близкую к стековым корутинам, но с динамическим стеком).
¶По модели взаимодействия
- Симметричные корутины: Все корутины равноправны. Любая корутина может передать управление любой другой корутине. Пример: корутины в Modula-2.
- Асимметричные корутины: Существует иерархия — корутина передаёт управление своему вызывающему коду (обычно с помощью
yieldилиawait). Вызывающий код решает, когда возобновить корутину. Пример: генераторы Python,async/awaitв C#.
¶По области применения
- Генераторы: Разновидность корутин, предназначенная для ленивой генерации последовательностей значений. Используют ключевое слово
yieldдля возврата значения и приостановки. Пример:yieldв Python,yieldв JavaScript. - Асинхронные корутины: Используются для асинхронного ввода-вывода. Приостанавливаются при ожидании внешнего события (сетевой запрос, чтение файла) и возобновляются после его завершения. Пример:
async/awaitв Python, C#, JavaScript, Rust. - Корутины для кооперативной многозадачности: Используются в игровых движках (Unity, Unreal Engine) для реализации таймеров, анимаций, последовательностей действий без блокировки основного потока.
¶Реализация в языках программирования
¶Python
В Python корутины реализованы двумя способами:
- Генераторы (с версии 2.2): Используют
yieldдля возврата значений. С версии 2.5 генераторы поддерживают двустороннюю связь через методыsend(),throw()иclose(). - Асинхронные корутины (с версии 3.5): Используют ключевые слова
async defиawait. Работают в рамках цикла событий (например,asyncio). Пример:
``python async def fetch_data(): await asyncio.sleep(1) return "data" ``
¶C++
В C++20 корутины реализованы через ключевые слова co_await, co_yield, co_return. Программист должен определить объект-обещание (promise) и объект-фрейм корутины, которые управляют её состоянием. Пример: ``cpp generator<int> range(int start, int end) { for (int i = start; i < end; ++i) { co_yield i; } } ``
¶Go
В Go аналогом корутин являются горутины (goroutines). Они представляют собой легковесные потоки, реализованные на уровне пользователя. Горутины имеют динамический стек, который может расти и сжиматься. Взаимодействие между горутинами осуществляется через каналы (channels). Пример: ``go func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { results <- j * 2 } } ``
¶JavaScript
В JavaScript корутины реализованы через асинхронные функции (async/await) и генераторы (function*). Асинхронные функции возвращают Promise, а генераторы — итераторы. Пример: ``javascript async function fetchData() { const response = await fetch('https://api.example.com'); return response.json(); } ``
¶Lua
Lua поддерживает стековые асимметричные корутины через библиотеку coroutine. Функции coroutine.create(), coroutine.resume(), coroutine.yield() позволяют создавать и управлять корутинами. Пример: ``lua co = coroutine.create(function() for i = 1, 5 do coroutine.yield(i) end end) ``
¶Применение
¶Асинхронный ввод-вывод
Корутины широко используются для реализации асинхронных серверов и клиентов. Вместо создания потоков для каждого соединения (что дорого), корутины позволяют обрабатывать тысячи одновременных подключений в одном потоке. Примеры: библиотека asyncio в Python, Tokio в Rust, Node.js (использует колбэки, но корутины через async/await).
¶Кооперативная многозадачность
В игровых движках корутины используются для реализации последовательностей действий, которые должны выполняться с задержками или в определённом порядке. Например, в Unity корутины позволяют выполнять анимацию, ожидание, загрузку ресурсов без блокировки основного потока.
¶Конечные автоматы
Корутины естественным образом представляют конечные автоматы: каждое состояние — это точка приостановки, а переходы — это возобновление корутины. Это упрощает реализацию протоколов, парсеров, игровых AI.
¶Генерация последовательностей
Генераторы (частный случай корутин) используются для ленивой генерации больших последовательностей данных, которые не помещаются в память целиком. Пример: чтение строк из файла, генерация чисел Фибоначчи.
¶Критика и ограничения
- Сложность отладки: Корутины могут затруднять отладку, так как стек вызовов может быть нелинейным (корутина может быть приостановлена в одном месте, а возобновлена в другом).
- Проблемы с исключениями: В некоторых реализациях (например, C++20) обработка исключений внутри корутин может быть нетривиальной.
- Отсутствие стандартизации: В разных языках корутины реализованы по-разному, что затрудняет перенос кода.
- Риск переполнения стека: Стековые корутины могут потреблять много памяти, если стек не ограничен. Бесстековые корутины ограничены в возможности вложенных вызовов.
- Кооперативность: Если корутина не передаёт управление (например, из-за бесконечного цикла), она может заблокировать весь поток.
¶См. также
- Подпрограмма
- Поток (thread)
- Асинхронное программирование
- Генератор (программирование)
- Конечный автомат
BFOmetr — база данных и аналитика по компаниям России.
На главную BFOmetr →


