Подробно о корутинах в C++

Автор оригинала: Dawid Pilarski
  • Перевод
Здравствуйте, коллеги.

В рамках проработки темы С++20 нам в свое время попалась уже довольно старенькая (сентябрь 2018) статья из хаброблога «Яндекса», которая называется "Готовимся к С++20. Coroutines TS на реальном примере". Заканчивается она следующей весьма выразительной голосовалкой:



«Почему бы и нет», — решили мы и перевели статью Давида Пиларски (Dawid Pilarski) под названием «Coroutines introduction». Статья вышла чуть более года назад, но, надеемся, все равно покажется вам очень интересной.

Итак, свершилось. После долгих сомнений, споров и подготовки этой фичи WG21 пришел к общему мнению о том, как должны выглядеть корутины в С++ — и весьма вероятно, что они войдут в C++ 20. Поскольку это крупная фича, думаю, пора готовиться и изучать ее уже сейчас (как помните, предстоит учить еще модули, концепции, диапазоны…)

Очень многие до сих пор выступают против корутин. Зачастую жалуются на сложность их освоения, множество точек кастомизации и, возможно, неоптимальной производительности из-за, возможно, недооптимизированного выделения динамической памяти (возможно ;)).

Параллельно с разработкой одобренных (официально опубликованных) технических спецификаций (ТС) даже предпринимались попытки параллельной разработки другого механизма корутин. Здесь мы поговорим именно о тех корутинах, которые описаны в TS (технической спецификации). Альтернативный подход, в свою очередь, принадлежит Google. В итоге оказалось, что и подход Google страдает от многочисленных проблем, для решения которых зачастую требуются странные дополнительные возможности C++.

В конце концов, было решено принять версию корутин, разработанных Microsoft (авторами TS). Именно о таких корутинах и пойдет речь в этой статье. Итак, начнем с вопроса о том…

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


Корутины уже существуют во многих языках программирования, например, в Python или C#. Корутины – это еще один способ создания асинхронного кода. Чем они отличаются от потоков, почему корутины должны быть реализованы как выделенная языковая возможность и, наконец, какая от них польза – будет объяснено в этом разделе.

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

  • Бесстековые корутины
  • Стековые корутины
  • Зеленые потоки
  • Волокна
  • Горутины

Хорошая новость: стековые корутины, зеленые потоки, волокна и горутины суть одно и то же (но используются они иногда по-разному). О них мы поговорим ниже в этой статье и будем называть их волокнами или стековыми корутинами. Но у бесстековых корутин есть некоторые особенности, о которых необходимо поговорить отдельно.

Чтобы понять корутины, в том числе, и на интуитивном уровне, давайте кратко познакомимся с функциями и (позволим себе такое выражение) “их API”. Стандартный способ работы с ними – вызвать и дожидаться, пока она завершится:

void foo(){
     return; // здесь мы выходим из функции
}	
foo(); // здесь мы вызываем/запускаем функцию

После вызова функцию уже невозможно приостановить, или возобновить ее работу. Над функциями можно производить всего две операции: start и finish. Когда функция запущена, необходимо дожидаться, пока она завершится. Если функция будет вызвана повторно, то ее выполнение пойдет с самого начала.

С корутинами ситуация иная. Их можно не только запускать и останавливать, но также приостанавливать и возобновлять. Они все равно отличаются от потоков ядра, поскольку сами по себе корутины не являются вытесняющими (с другой стороны, корутины обычно относятся к потоку, а поток является вытесняющим). Чтобы разобраться в этом, рассмотрим генератор, определенный на Python. Пусть в Python такая штука и называется генератором, в языке C++ она называлась бы корутиной. Пример взят с этого сайта:

def generate_nums():
     num = 0
     while True:
          yield num
          num = num + 1	

nums = generate_nums()
	
for x in nums:
     print(x)
	
     if x > 9:
          break

Вот как работает этот код: вызов функции generate_nums приводит к созданию объекта корутины. На каждом этапе перебора объекта корутины, сама корутина возобновляет работу и приостанавливает ее только после ключевого слова yield в коде; тогда же возвращается следующее целое число из последовательности (цикл for представляет собой синтаксический сахар для вызова функции next(), возобновляющей корутину). Код завершает цикл, встретив инструкцию break. В данном случае корутина не заканчивается никогда, но легко представить ситуацию, в которой корутина достигает конца и завершается. Как видим, к такой корутине применимы операции start, suspend, resume и, наконец, finish. [Замечание: в языке C++ также предусмотрены операции создания и разрушения, но они не важны в контексте интуитивного понимания корутины].

Корутины как библиотека


Итак, теперь примерно понятно, что представляют из себя корутины. Возможно, вам известно, что существуют библиотеки для создания объектов-волокон. Вопрос в том, зачем нам корутины в виде выделенной языковой возможности, а не просто библиотеки, которая обеспечивала бы работу с корутинами.

Здесь мы пытаемся ответить на этот вопрос и продемонстрировать разницу между стековыми и бесстековыми корутинами. Эта разница имеет ключевое значение для понимания корутин как части языка.

Стековые корутины


Итак, давайте сначала обсудим, что такое стековые корутины, как они работают, и почему их можно реализовать как библиотеку. Объяснить их сравнительно просто, поскольку по устройству они напоминают потоки.

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

  • У них есть собственный стек,
  • Время жизни волокон не зависит от кода, который их вызывает (обычно у них есть планировщик, определяемый пользователем),
  • Волокна можно откреплять от одного потока и прикреплять к другому,
  • Кооперативное планирование (волокно должно принимать решение о переключении на другое волокно/планировщик),
  • Не могут работать одновременно в одном и том же потоке.

Из вышеупомянутых свойств проистекают такие следствия:

  • переключение контекста волокон должно осуществляться самим пользователем волокон, а не ОС (кроме того, ОС может отпустить волокно, отпустив тот поток, в котором оно работает),
  • Не возникает никакой реальной гонки данных между двумя волокнами, поскольку в любой момент времени активно может быть лишь одно из них,
  • Разработчик волокон должен уметь правильно выбирать место и время, где и когда уместно возвращать вычислительные мощности возможному планировщику или вызывающей стороне.
  • Операции ввода/вывода в волокне должны быть асинхронными, так, чтобы другие волокна могли выполнять свои задачи, не блокируя друг друга.

Теперь давайте детально разберемся в работе волокон и для начала объясним, как стек участвует в вызовах функций.

Итак, стек – это непрерывный блок памяти, нужный для хранения локальных переменных и аргументов функций. Но, что еще важнее, после каждого вызова функции (за немногими исключениями) в стек помещается дополнительная информация, сообщающая вызванной функции, как возвратиться к вызывающей стороне и восстановить регистры процессора.

У некоторых из этих регистров есть особые назначения, и при вызовах функций они сохраняются в стеке. Вот какие это регистры (в случае архитектуры ARM):

SP – указатель стека
LR – регистр связи
PC – счетчик программ

Указатель стека (SP) – это регистр, в котором содержится адрес начала стека, относящегося к текущему вызову функции. Благодаря имеющемуся значению, можно без труда ссылаться на аргументы и локальные переменные, сохраняемые в стеке.

Регистр связи (LR) очень важен при вызовах функций. В нем хранится адрес возврата (адрес вызывающей стороны), где должен будет выполняться код после того, как выполнение текущей функции завершится. При вызове функции PC сохраняется в LR. При возврате функции PC восстанавливается при помощи LR.

Счетчик программ (PC) – это адрес инструкции, выполняемой в данный момент.
Всякий раз при вызове функции список связей сохраняется, так что функции известно, куда должна вернуться программа после того как она завершится.



Поведение регистров PC и LR при вызове и возврате функции

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



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

  1. Обычный вызов функции внутри потока. Выделение памяти производится в стеке.
  2. Функция создает объект волокна. В результате выделяется стек под волокно. Создание волокна еще не означает, что оно сразу же будет выполнено. Также выделяется фрейм активации. Данные во фрейме активации заданы таким образом, что сохранение его содержимого в регистры процессора приведет к переключению контекста на стек волокна.
  3. Обычный вызов функции.
  4. Вызов корутины. Для регистров процессора задается контент фрейма активации.
  5. Обычный вызов функции внутри корутины.
  6. Обычный вызов функции внутри корутины.
  7. Корутина приостанавливается. Содержимое фрейма активации обновляется, и устанавливаются регистры процессора, так, что контекст возвращается к стеку потока.
  8. Обычный вызов функции внутри потока.
  9. Обычный вызов функции внутри потока.
  10. Возобновление корутины – происходит примерно то же самое, что и при вызове корутины.
  11. Фрейм активации запоминает состояние тех регистров процессора внутри корутины, которые были установлены при приостановке корутины.
  12. Обычный вызов функции внутри корутины. Фрейм функции выделяется в стеке корутины.
  13. Ситуация на картинке показана в несколько упрощенном виде. Теперь происходит вот что: работа корутины заканчивается, и весь стек раскручивается. Однако, на самом деле возврат из корутины происходит через нижнюю (а не верхнюю) функцию.
  14. Обычный возврат функции, как и выше.
  15. Обычный возврат функции.
  16. Возврат корутины. Стек корутины пуст. Контекст переключается обратно к потоку. Начиная с этого момента, работа волокна не может быть возобновлена.
  17. Обычный вызов функции в контексте потока.
  18. Позже функции могут продолжать работу или завершаться, так, что раскрутка стека полностью завершится.

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

swtch.com/libtask
code.google.com/archive/p/libconcurrency
www.boost.org Boost.Fiber
www.boost.org Boost.Coroutine

Из всех этих библиотек только Boost относится к C++, а все остальные — к C.
Подробное описание работы этих библиотек приводится в документации. Но, в целом, все эти библиотеки позволяют создать отдельный стек для волокна и предоставляют возможность возобновить корутину (по инициативе вызывающей стороны) и приостановить ее (изнутри).

Рассмотрим пример Boost.Fiber:

#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
#include <thread>
	
#include <boost/intrusive_ptr.hpp>
	
#include <boost/fiber/all.hpp>
	
inline
void fn( std::string const& str, int n) {
     for ( int i = 0; i < n; ++i) {
          std::cout << i << ": " << str << std::endl;
               boost::this_fiber::yield();
     }
}
	
int main() {
     try {
          boost::fibers::fiber f1( fn, "abc", 5);
          std::cerr << "f1 : " << f1.get_id() << std::endl;
          f1.join();
          std::cout << "done." << std::endl;
	
          return EXIT_SUCCESS;
     } catch ( std::exception const& e) {
          std::cerr << "exception: " << e.what() << std::endl;
     } catch (...) {
          std::cerr << "unhandled exception" << std::endl;
     }
     return EXIT_FAILURE;
}

В случае Boost.Fiber в библиотеке есть встроенный планировщик для корутин. Все волокна выполняются в одном и том же потоке. Поскольку планирование корутин является кооперативным, волокно сначала должно определиться с тем, когда возвращать контроль планировщику. В данном примере это происходит при вызове функции yield, которая приостанавливает корутину.

Поскольку другого волокна нет, планировщик волокон всегда решает возобновить работу корутины.

Бесстековые корутины


Бесстековые корутины немного отличаются по свойствам от стековых. Однако, основные характеристики у них те же, поскольку бесстековые корутины так же можно запускать, а после их приостановки – возобновлять. Корутины именно такого типа мы, скорее всего, найдем в C++20.

Если говорить о схожих свойствах корутин – корутины могут:

  • Корутина тесно связана со своей вызывающей стороной: при вызове корутины исполнение передается ей, а итог работы корутины передается обратно вызывающей стороне.
  • Длительность жизни стековой корутины равна жизни ее стека. Длительность жизни бесстековой корутины равна жизни ее объекта.

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

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

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

Взгляните, как работают бесстековые корутины:



Вызов бесстековой корутины

Как видим, теперь стек всего один – это главный стек потока. Давайте пошагово разберем, что показано на этой картинке (фрейм активации корутины здесь двухцветный – черным показано, что сохранено в стеке, а синим – что сохранено в куче).

  1. Обычный вызов функции, чей фрейм сохраняется в стеке
  2. Функция создает корутину. То есть, выделяет для нее фрейм активации где-нибудь в куче.
  3. Обычный вызов функции.
  4. Вызов корутины. Тело корутины выделяется в обычном стеке. Программа выполняется таким же образом, как и в случае обычной функции.
  5. Обычный вызов функции из корутины. Опять же, все по-прежнему происходит в стеке [Примечание: из этой точки корутину приостановить нельзя, поскольку это не самая верхняя функция в корутине]
  6. Функция возвращается к самой верхней функции корутины [Примечание: теперь корутина может приостановить себя.]
  7. Корутина приостанавливается – все данные, которые нужно было сохранить из всех вызовов корутин, теперь записываются во фрейм активации.
  8. Обычный вызов функции
  9. Корутина возобновляет работу – это происходит как обычный вызов функции, но с прыжком к предыдущей точке приостановки + восстановлением переменных из фрейма активации.
  10. Вызов функции как в пункте 5.
  11. Возврат функции как в пункте 6.
  12. Возврат корутины. С этого момента возобновить работу корутины уже невозможно.

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

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

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

Практическое применение корутин


Корутины в C++ могут использоваться такими же способами, как и в других языках. Корутины позволят упростить написание:

  • генераторов
  • асинхронного кода ввода/вывода
  • ленивых вычислений
  • событийно-ориентированных приложений

Итоги


Надеюсь, что, прочитав эту статью, вы узнали:

  • почему в C++ требуется реализовать корутины в виде выделенной языковой возможности
  • в чем разница между стековыми и бесстековыми корутинами
  • зачем нужны корутины
Издательский дом «Питер»
Компания

Похожие публикации

Комментарии 19

    +3

    А можете привести более менее реальный пример кода с корутинами и без, чтобы увидеть все преимущества.

      +1

      Что-то я не совсем представляю как будет выглядеть асинхронный ввод-вывод, если yield можно делать только на самом верхнем уровне.


      И что с исключениями, кстати?

        +1
        > Что-то я не совсем представляю как будет выглядеть асинхронный ввод-вывод

        Как я понимаю, в этом случае корутина не будет отличаться от green thread-а. То есть точно также придется писать планировщик корутин и т.д. То есть, все то, что и так делает ОС с потоками при вводе-выводе

        У меня есть ощущение, что безстековые корутины имеют преимущество разве что во всяких генераторах/ленивых вычислениях
          0
          Безстековые корутины отлично сочетаются с ranges… есть только одна проблема: компиляторы генерят, пока что, отвратительный код с ними.

          То есть повторяется история с STL: задизайнили очередную zero-cost abstraction… только вот на практике cost там очень даже не zero и потребуется лет 10, пока он станет zero.
            0
            Ну в ranges как раз генераторы, как я понимаю.

            Еще возникают мысли, что корутины можно использовать для линеаризации кода, но пока не сильно задумывался, насколько сложно это сделать
        0
        Что то мне подсказывает что эти нововведения породят еще больше бардака и новых умных указателей или даже строк.
        Пока не будет вменяемого контроля за ресурсами будут получаться только макароны.
        Даже сейчас вы не можете завершить поток при этом быть уверенным что освободили все ресурсы и остановили все потоки который тот успел породить. Более того вы не можете ограничит поток в ресурсах или времени выполнения. Особенно когда используете сторонние библиотеки.
        Так что все эти нововведения будут порождать только печаль и грусть, зато много.
          0
          Даже сейчас вы не можете завершить поток при этом быть уверенным что освободили все ресурсы и остановили все потоки который тот успел породить.

          Я всё могу:


          my_thread.cancel(); // сообщаем потоку что ему пора остановиться.
          my_thread.join(); // дожидаемся пока поток остановится
          
          // В этой точке есть гарантия, что все ресурсы освобождены и дочерние потоки остановлены.
            +1
            Ну вы же сами написали: «сообщаем потоку что ему пора остановиться». Это совсем-совсем не то же самое, что «принудительно завершить поток».

            Там на самом деле есть фундаментальные проблемы, которые не позволяют это сделать надёжно. Вот тут есть древняя статья на эту тему.

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

            Впрочем боль вашего оппонента… она такая — фантомная. Если вы хотите, чтобы «завершить поток при этом быть уверенным что освободили все ресурсы и остановили все потоки который тот успел породить» — то для этого достаточно использовать новинку, суперпродвинутую технологию, которая появилась в IBM System/360 Model 67. Процессы называются. Да, я знаю, 55 лет — это очень мало, за такое время не все смогли понять как этим пользоваться… и, тем не менее, попробуйте: оно специально для этого и предназначено.

            </sarcasm>

            А если серьёзно — то тут у нас, как бы, некоторое недоперепонимание. Потоки — это такая специальная технология, которая позволяет все ресурсы в нескольких программах сделать общими (во всяком случае ровно так оно реализовано, скажем в Linux). И после этого вы хотите, чтобы кто-то вам эти ресурсы отделил? Я, типа извиняюсь, как? Залезть вам в голову и прочитать там гениальный план по захвату мира? Пока таких технологий не существует.

            Если вы изначально занялтись делением вашей системы на части, то есть много способов — cgroups, Job Objects и так далее.

            Если бы это реально было кому-то нужно и люди бы заботились о безопасности по настоящему (а не ограничивались жалобами на то, что кто-то другой о ней не заботится на Хабре) — то и поддержка в языке для всего этого, скорее всего, появилась бы…
              0
              Не согласен, вижу тут аналогию с деструкторами и RAII. Вызывая деструктор, вы точно так же говорите объекту освободить ресурсы, но нет гарантий, что объект спроектирован корректно и эти ресурсы освободит. Тем не менее, претензий к этому не возникает, потому что есть RAII — идиома, позволяющая создать такую гарантию. Если класс спроектирован корректно (RAII), то есть гарантия освобождения ресурсов.

              То же самое с потоками. Чтобы мочь останавливать поток, сам поток (его функция) должны быть спроектированы так, чтобы поддерживать остановку. Тогда можно создать гарантию освобождения ресурсов. И не важно, принудительно мы что то завершаем или просим поток. Объект вы тоже принудительно не отчистите.

              Насчёт того, что таких средств нет…
              На самом деле в с++20 для этого даже есть стандартный класс потоков — std::jthread, но впрочем не сложно и свой навелосипедить.
                0
                Если класс спроектирован корректно (RAII), то есть гарантия освобождения ресурсов.
                Можно и без RAII всё нормально сделать и с RAII набедокурить. Прочитайте ещё раз на что вы отвечали:
                Даже сейчас вы не можете завершить поток при этом быть уверенным что освободили все ресурсы и остановили все потоки который тот успел породить. Более того вы не можете ограничит поток в ресурсах или времени выполнения. Особенно когда используете сторонние библиотеки.

                Здесь явно речь не идёт о «корректной работе» и «правильном использовании». Это чёткий запрос на процессы, cgroups и прочее по списку.

                Но ведь хочется и на «и рыбку съесть, и на елку влезть» — и не платить за гарантии ничего и получить именно гарантии, а не благие пожелания. Но так не бывает, за всё нужно платить: если вы свалили в кучу ресурсы, выделенные для разных целей, устроили у себя полный MS-DOS — то гарантированно «расплести их» «в случае чего» — уже не удастся. Хоть с RAII, хоть без RAII…
              0
              Ага точно, особено если my_thread использует десяток сторонних библиотек, которые тоже используют потоки, сетевые соединения и работают с файлами и при этом не содержат ошибок, дедлоков и других подстав.
            +1
            Как очень сложно рассказать о простых вещах…
            Ну и простите, не удержался —
            Как я обычно вижу сишный код:
            #include <cstdlib>
            #include <iostream>
            #include <memory>
            #include <string>
            #include <thread>
            	
            #include <boost/intrusive_ptr.hpp>
            	
            #include <boost/fiber/all.hpp>
            	
            inline
            void fn(::&, int n) {
            ( = ; < ; ++) {
            :: << << ": " << << ::;
            ::_::();
            }
            }
            	
            int main() {
            {
            ::::( fn, "abc", 5);
            :: << "f1 : " << .() << ::;
            .();
            :: << "done." << ::;
            	
            return EXIT_SUCCESS;
            } ( ::& ) {
            :: << "exception: " << .() << ::;
            } (...) {
            :: << "unhandled exception" << ::;
            }
            return EXIT_FAILURE;
            }

              +2
              По моему личному мнению, статья неудачная. И оригинал, и перевод. Читается с трудом, как будто продираешься сквозь заросли кустарника.

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

              Ещё сугубо IMHO. Не конкретно в отношении статьи, но всё-же. Что за дикая калька с английского — «корутина»? Я когда в первый раз (относительно недавно) в русском тексте наткнулся на это слово, не сразу понял, что имеется ввиду. Оказывается это coroutine, для которого давно есть русский перевод: сопрограмма (по аналогии с подпрограммойsubroutine). Правда тогда не было Go с его горутинами. Но это так, тоже к слову.

              Итоги. Надеюсь, что, прочитав эту статью, вы узнали:
              • зачем нужны корутины

              Не узнал. С самой концепцией знаком давно, но практической необходимости в сопрограммах никогда не испытывал. Возможно повезло. Возможно, что сопрограммы действительно жизненно необходимы в какой-то очень узкой области. А раздел Практическое применение корутин подозрительно лаконичен.
              • почему в C++ требуется реализовать корутины в виде выделенной языковой возможности

              Не узнал. И полностью согласен с предыдущим комментарием, что это скорее добавит реальной боли, чем практической выгоды.
              • в чем разница между стековыми и бесстековыми корутинами

              Узнал (yahoo-o-o!). Только исходя из предыдущих двух абзацев, пока не понятно что с этим знанием делать.

              Далее, кто мне объяснит принципиальное отличие примера на Питоне вот от этого:
              #include <iostream>
              
              class generateNums {
              	int m_num;
              public:
              	generateNums() : m_num(0) {}
              	int operator () () { return m_num++; }
              };
              
              int main() {
              	generateNums nums;
              	int x;
              
              	while (true) {
              		x = nums();
              		std::cout << x << std::endl;
              		if (x > 9)
              			break;
              	}
              }
              

                0
                Далее, кто мне объяснит принципиальное отличие примера на Питоне вот от этого:
                Принципиальное отличие — такое же как отличие программы на C++, Python или Java от «классического» BASIC'а (того, где есть только IFGOTO и нет IFTHENELSEENDIF).

                Вот в точности: любая пограмма написанная в парадигме структурного программирования может быть реализована с помощью «спагетти-кода».

                То же самое с корутинами: всё что они позволяют сделать можно выразить с помощью конечного автомата и стека (в вашем, простейшем, случае, без стека удалось обойтись… в общем случае это невозможно).

                Простейший практический пример: имеется два DOM-дерева (с разными пометками, вложенными DIV'ами и прочим), мы хотим их сравнить на тему — онаков ли у них #innerText или нет (без материализации, конечно: C++, экономия памяти, всё такое).

                В случае с корутинами — берётся простейший рекурсивыный обход дерева, где-то в его глубине появляется co_yield — и всё красиво и понятно.

                В случае с вашим подходом… ну… всё тоже делается — но стек придётся завести явно (можно, конечно, смухлевать и при «возврате» заново искать нашу ноду в массиве children и перейти к следующей — но тут будет уже квадратичная сложность вместо линейной).

                Как-то примерно так.

                И да, если бы вместо жонглирования терминами было бы показано, на примерах, что и как бывает с корутинами (на самом-то деле вы наверняка их «руками» реализовывали не раз, как ваш пример с generateNums показывает) — то было бы понятнее. Любая функция типа ProcessSomething(callback, userdata) (которых в виденных мною API бывает десятками и сотнями) — это «реализованная руками» корутина.

                P.S. На практике stackless корутины — самый удобный способ создания ranges, но, увы, порождаемый современными компиляторами код не вызывает желания их в таком качестве использовать. Но лет через 3-5-10… всё может измениться. Подождём, посмотрим…
                  0
                  Статья действительно не очень, честно говоря, ну разве что в плане освещения разницы между stackless и stackful. В плане «зачем нужны» лучше почитать другую статью, на которую есть ссылка в начале этой статьи, там этот вопрос как-то более подробно освещен на примере асинхронной работы с сетью. Хотя лично мне для организации цепочек асинхронных операций пока больше нравится подход в стиле «классических» Promises/A+, конкретно я кое-где пользовался вот этой библиотекой, в принципе, довольно удобно.
                    0
                    по идее, тоже самое можно было сделать и статической переменной?
                      +1
                      Нет. Статической переменной не получится, если нужно несколько независимых генераторов.
                      0

                      Ну здесь две сущности — генератор и главный цикл.
                      В одной программе да, можно вызвать одно из другого.
                      А попробуйте их повесить на канал (например, на сокет или пайп). Чтоб генератор свои числа пихал в один конец, цикл читал значения из другого. И да, чтобы при этом программа осталась однопоточной. И чтобы она при этом не превратилась в огромный файл с легаси-велосипедами.

                      0
                      Почему-то в статье первые две картинки одинаковые (которые про стек).

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое