Введение
Впервые я столкнулся с корутинами в одном из проектов. В нем было принято решение использовать реактивный подход. До этого я не работал с реактивщиной и решил разобраться с тем, что за зверь такой эти "корутины". Я начну от идеи и истоков и надеюсь дойти до понимания реализации в Kotlin. Мне удалось найти статью Design of a Separable Transition-diagram Compiler от 1963 года. Люди пишут, что это одно из самых ранних упоминаний идеи корутин. В статье есть часть с названием "Coroutines and Separable Programs", про которую я узнал из поста Why using Kotlin Coroutines?. С этой части, а точнее с ее перевода я и начну.
Корутины и разделяемые программы
Программа является разделяемой, если она может быть "разбита" (break up) на отдельные модули, которые обмениваются данными с учетом следующих ограничений:
обмен данными происходит в форме передачи дискретных значений информации;
поток данных однонаправленный и фиксированного размера;
программа представлена как набор модулей, у которых слева расположены входы (прим. прием данных), а справа выходы и информационный поток движется между модулями слева направо. (Прим. pipeline)
При соблюдении этих условий каждый модуль может быть превращен в сопрограмму (далее корутину), то есть может быть закодирован как отдельная программа, которая обменивается данными со смежными модулями, принимая входные данные, обрабатывая и передавая их на выход. Таким образом корутины это подпрограммы, расположенные на одном уровне исполнения, так как если бы они исполнялись main программой, хотя на самом деле никакой main программы нет. Нет никаких ограничений на количество входов и выходов, которое может иметь корутина. Концепция корутин может заметно упростить концепцию программы, когда модули не обмениваются данными в синхронном режиме. В качестве примера рассмотрим программу чтения данных с перфокарт и записи символов, которые будут считаны с 1-й до 80-й колонки первой карты, потом второй и т.д. с учетом того, что каждый раз, когда встречаешь два символа * нужно заменить их на символ "↑". Блок-схема такой программы, представленная как набор процедур, выполняемых в main программе изображена на рисунке 1.
![Рисунок 1. Процедурный подход. Рисунок 1. Процедурный подход.](https://habrastorage.org/getpro/habr/upload_files/491/0a9/704/4910a9704a5c2aa7ee663e001c25c73d.png)
Отметим, что необходимость схлопывания символа '*' требует добавления переключателя (switch), который разделяет поток выполнения и выбирает ветку, по которой программа будет выполняться в зависимости от результата последнего вызова. Причина для использования switch в том, что каждый вызов должен выводить ровно один символ.
Подход, к той же задаче, основанный на корутинах выполняет переключение неявно, используя последовательность вызовов подпрограмм. Когда корутины А и B соединены так, что А отправляет данные к B, B работает до тех пор, пока не получит команду на чтение. Потом управление будет передано к А пока она не "решит" записать данные, после чего управление вернется к B в точку, из которой она передала управление ранее. На рисунке 2 представлена схема программы для сжатия звездочек, когда обе программы являются корутинами.
![Рисунок 2. Подход, основанный на корутинах. Рисунок 2. Подход, основанный на корутинах.](https://habrastorage.org/getpro/habr/upload_files/f44/493/817/f44493817767753ddbf1584a833345ac.png)
Рисунок 3 иллюстрирует суть разделяемости (separability). Чтобы модули А и В не передавали управление туда-сюда каждый раз когда один из символов прочитан/записан можно без изменения А или В позволить А записать все данные на ленту, перемотать ленту и потом позволить В считать символы с ленты. В этом смысле программы А и В могут работать как однопроходной или двухпроходной обработчик с простейшими изменениями.
![Рисунок 3. Рисунок 3.](https://habrastorage.org/getpro/habr/upload_files/304/036/4e2/3040364e27e62118023d6158389d34b8.png)
Выводы
Насколько я понял для Конвэя было важно сделать программу однопроходной, то есть вначале считать данные, а потом записать на ленту, вместо чтения/записи по одному символу. Он придумал разделить программу на независимые модули и передавать потоки управления от одной программы другой. Отметим для себя некоторые важные моменты:
Программу можно представить как отдельные модули со входами и выходами, которые передают друг другу данные. Эти модули есть сопрограммы или корутины.
Проверку выполнения условий перехода от одной ветки программы к другой можно заменить на неявное переключение управления.
Возвращать управление нужно по адресу, из которого была выполнена передача управления.