Как стать автором
Обновить

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

Время на прочтение10 мин
Количество просмотров34K
Всего голосов 17: ↑16 и ↓1+15
Комментарии19

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

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

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


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

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

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

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

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

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

Я всё могу:


my_thread.cancel(); // сообщаем потоку что ему пора остановиться.
my_thread.join(); // дожидаемся пока поток остановится

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

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

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

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

</sarcasm>

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

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

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

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

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

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

Но ведь хочется и на «и рыбку съесть, и на елку влезть» — и не платить за гарантии ничего и получить именно гарантии, а не благие пожелания. Но так не бывает, за всё нужно платить: если вы свалили в кучу ресурсы, выделенные для разных целей, устроили у себя полный MS-DOS — то гарантированно «расплести их» «в случае чего» — уже не удастся. Хоть с RAII, хоть без RAII…
Ага точно, особено если my_thread использует десяток сторонних библиотек, которые тоже используют потоки, сетевые соединения и работают с файлами и при этом не содержат ошибок, дедлоков и других подстав.
Как очень сложно рассказать о простых вещах…
Ну и простите, не удержался —
Как я обычно вижу сишный код:
#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;
}

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

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

Ещё сугубо 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;
	}
}

Далее, кто мне объяснит принципиальное отличие примера на Питоне вот от этого:
Принципиальное отличие — такое же как отличие программы на 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… всё может измениться. Подождём, посмотрим…
Статья действительно не очень, честно говоря, ну разве что в плане освещения разницы между stackless и stackful. В плане «зачем нужны» лучше почитать другую статью, на которую есть ссылка в начале этой статьи, там этот вопрос как-то более подробно освещен на примере асинхронной работы с сетью. Хотя лично мне для организации цепочек асинхронных операций пока больше нравится подход в стиле «классических» Promises/A+, конкретно я кое-где пользовался вот этой библиотекой, в принципе, довольно удобно.
по идее, тоже самое можно было сделать и статической переменной?
Нет. Статической переменной не получится, если нужно несколько независимых генераторов.

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

Почему-то в статье первые две картинки одинаковые (которые про стек).
Зарегистрируйтесь на Хабре, чтобы оставить комментарий