Ответ 1
На абстрактном уровне Coroutines отделил идею наличия состояния выполнения от идеи наличия потока выполнения.
SIMD (одна команда, несколько данных) имеет несколько "потоков выполнения", но только одно состояние выполнения (оно работает только с несколькими данными). Возможно, параллельные алгоритмы немного похожи на это в том, что у вас есть одна "программа", запущенная на разных данных.
Потоки имеют несколько "потоков выполнения" и несколько состояний выполнения. У вас есть более одной программы и более одного потока выполнения.
Coroutines имеет несколько состояний выполнения, но не владеет потоком выполнения. У вас есть программа, и программа имеет состояние, но у нее нет потока выполнения.
Самым простым примером сопрограмм являются генераторы или перечислимые элементы из других языков.
В псевдокоде:
function Generator() {
for (i = 0 to 100)
produce i
}
Generator
вызывается, и при первом вызове он возвращает 0
. Его состояние запоминается (насколько состояние изменяется в зависимости от реализации сопрограмм), и в следующий раз, когда вы его называете, оно продолжается там, где остановилось. Так что возвращается 1 в следующий раз. Тогда 2.
Наконец, он достигает конца цикла и падает с конца функции; сопрограмма закончена. (То, что здесь происходит, зависит от языка, о котором мы говорим; в python это вызывает исключение).
Сопрограммы доводят эту возможность до C++.
Есть два вида сопрограмм; стека и без стека.
Сопрограмма без стеков хранит только локальные переменные в своем состоянии и месте выполнения.
Стековая сопрограмма хранит весь стек (например, поток).
Стеки-сопрограммы могут быть чрезвычайно легкими. Последнее предложение, которое я прочитал, касалось в основном переписывания вашей функции во что-то вроде лямбды; все локальные переменные переходят в состояние объекта, а метки используются для перехода в/из места, где сопрограмма "выдает" промежуточные результаты.
Процесс создания значения называется "yield", поскольку сопрограммы напоминают кооперативную многопоточность; Вы возвращаете точку исполнения обратно вызывающей стороне.
Boost имеет реализацию стековых сопрограмм; это позволяет вам вызывать функцию для вас. Stackful сопрограммы являются более мощными, но и более дорогими.
В сопрограммах есть нечто большее, чем простой генератор. Вы можете ожидать сопрограмму в сопрограмме, которая позволяет вам составлять сопрограммы в полезной манере.
Сопрограммы, такие как if, циклы и вызовы функций, представляют собой еще один вид "структурированного перехода", который позволяет более естественным образом выражать определенные полезные шаблоны (например, конечные автоматы).
Конкретная реализация сопрограмм в C++ немного интересна.
На самом базовом уровне он добавляет несколько ключевых слов в C++: co_return
co_await
co_yield
вместе с некоторыми типами библиотек, которые работают с ними.
Функция становится сопрограммой, имея одну из них в своем теле. Таким образом, из их объявления они неотличимы от функций.
Когда одно из этих трех ключевых слов используется в теле функции, происходит некоторое стандартное обязательное изучение типа и аргументов возврата, и функция превращается в сопрограмму. Это исследование говорит компилятору, где хранить состояние функции, когда функция приостановлена.
Самая простая сопрограмма - это генератор:
generator<int> get_integers( int start=0, int step=1 ) {
for (int current=start; true; current+= step)
co_yield current;
}
co_yield
приостанавливает выполнение функций, сохраняет это состояние в generator<int>
, а затем возвращает значение current
через generator<int>
.
Вы можете перебрать возвращенные целые числа.
co_await
позволяет вам соединить одну сопрограмму на другую. Если вы находитесь в одной сопрограмме и вам нужны результаты ожидаемой вещи (часто сопрограммы) до того, как вы co_await
прогрессировать, вы можете co_await
это. Если они готовы, вы продолжите немедленно; если нет, вы приостанавливаете работу, пока ожидающий, на котором вы ожидаете, не будет готов.
std::future<std::expected<std::string>> load_data( std::string resource )
{
auto handle = co_await open_resouce(resource);
while( auto line = co_await read_line(handle)) {
if (std::optional<std::string> r = parse_data_from_line( line ))
co_return *r;
}
co_return std::unexpected( resource_lacks_data(resource) );
}
load_data
- сопрограмма, которая генерирует std::future
когда именованный ресурс открыт, и нам удается проанализировать точку, в которой мы нашли запрошенные данные.
open_resource
и read_line
, вероятно, являются асинхронными сопрограммами, которые открывают файл и читают из него строки. co_await
связывает состояние приостановки и готовности load_data
с их прогрессом.
Сопрограммы C++ гораздо более гибкие, чем эта, поскольку они были реализованы как минимальный набор языковых функций поверх типов пользовательского пространства. Типы пользовательского пространства эффективно определяют, что co_return
co_await
и co_yield
- я видел, как люди используют его для реализации монадических необязательных выражений, так что co_await
для пустого необязательного параметра автоматически распространяет пустое состояние на внешнее необязательное:
modified_optional<int> add( modified_optional<int> a, modified_optional<int> b ) {
return (co_await a) + (co_await b);
}
вместо
std::optional<int> add( std::optional<int> a, std::optional<int> b ) {
if (!a) return std::nullopt;
if (!b) return std::nullopt;
return *a + *b;
}