Pull to refresh

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;

}


Это конечно не труъ-генераторы, но симулировать их поведение в ранних стандартах можно через лямбды (строго говоря, через функторы; лямбы -- сахар над ними)

Я это понимаю. Строго говоря и лямбды тоже сахар. Я без них больше 10 лет жил (писал на c++98), использовал классы и не обламывался.

А не могли бы вы рассказать что будет если мы выйдем из цикла раньше чем генератор закончит: например по break. А сам генератор по мере выполнения будет открывать файлы и запускать потоки.

Типа такого?
#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.

UFO just landed and posted this here
Sign up to leave a comment.