Comments 8
Маловато для статьи, на хабре уже была хорошая, подробная статья про корутины. А это выглядит как запись для блога. Где диаграмма передачи владения, подводные камни, как раз если бы вы написали про решение проблем которые вы обозначили (и от которых отмахнулись фразой, смотри в моей репе), было бы куда интереснее.
То что мне показалось интересным и касалось непосредственно корутин - я описал в комментариях. Решение описанных проблем - это скорее умение работать с std::exception_ptr и std::variant (ну или более традиционно - через указатели). Если погружаться во все детали - многовато получится :)
Статей про корутины действительно много, как и докладов на различных конференциях. Суть большинства из них тут. Я не ставил целью еще раз рассказать, как они работают, а хотел разобрать один вполне конкретный пример.
#include <iostream>
#include <optional>
auto generate(int from, int to)
{
return [=, cur = from]() mutable -> std::optional<int>
{
if(cur == to)
return std::nullopt;
return cur++;
};
}
int main()
{
auto gen = generate(0, 10);
while(auto val = gen())
{
std::cout << *val << std::endl;
}
return 0;
}
Это конечно не труъ-генераторы, но симулировать их поведение в ранних стандартах можно через лямбды (строго говоря, через функторы; лямбы -- сахар над ними)
#include <thread>
#include <future>
#include <chrono>
#include <fstream>
#include <iostream>
using namespace std;
size_t fn(size_t i) {
cout<<"processing "<<i<<"\n";
this_thread::sleep_for(chrono::milliseconds(500));
return i;
}
generator generate(size_t start, size_t end) {
enum { ahead=4 };
ofstream log("log.txt");
future<size_t> cache[ahead];
size_t h=start, t=start;
for(auto i=start; i<end; ++i) {
if (i>=t) {
h=t; t=h+ahead; if (t>end) t=end;
for(auto j=0; j<t-h; ++j) cache[j]=async(fn,i+j);
}
size_t res=cache[i-h].get();
log<<"i="<<i<<" res="<<res<<endl;
co_yield res;
}
}
int main() {
for(auto value: generate(0, 10)) {
cout<<value<<endl;
if (value==5) break;
}
return 0;
}
Написал длинный ответ и понял, что пишу не про то :) Вопрос отличный!
Если кратко, в вашем примере я проблем не вижу. Всё должно корректно отработать. Могу предположить, что в вашем примере после завершения цикла программа зависнет на полсекунды в деструкторе future, который будет вызван методом destroy() из деструктора generator.
Все локальные переменные корутины хранятся не в стеке, а в куче. Они будут освобождены вызовом m_coro.destroy() в порядке, обратном порядку создания. Тут действительно возможны подводные камни. Например, если вы используете конструкцию try { ... } catch для освобождения каких-то ресурсов. Если у вас везде RAII - проблем не будет.
Чтобы проверить самого себя, можно задать себе вопрос: что будет, если в момент одной из итераций co_yield превратится в return (для корутины co_return)? Если это не приведет к катастрофе, то и корутина нормально отработает.
Забавно то, что в своё время на питоне был веб-фреймворк ( Tornado
, вроде), который реализовывал корутины основываясь на генераторах. Тогда асинхрона в питоне не было, а генераторы были, вот и создал разраб асинхронные вызовы на основе yield
.
Генераторы на корутинах C++