Что такое сопрограммы в С++ 20?

Что такое сопрограммы в ?

Чем он отличается от "Parallelism2" или/и "Concurrency2" (см. Изображение ниже)?

Изображение ниже от ISOCPP.

https://isocpp.org/files/img/wg21-timeline-2017-03.png

enter image description here

Ответ 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;
}

Ответ 2

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

Предыдущая экспериментальная реализация coroutine от Microsoft использовала скопированные стеки, чтобы вы могли даже вернуться из глубоких вложенных функций. Но эта версия была отвергнута комитетом С++. Вы можете получить эту реализацию, например, с библиотекой волокон Boosts.

Ответ 3

Предполагается, что сопрограммы должны быть (в С++) функциями, которые могут "ждать" для выполнения какой-либо другой процедуры и предоставлять все необходимое для приостановленной, приостановленной, ожидающей, подпрограммы. функция, которая наиболее интересна для людей С++, заключается в том, что сопрограммы идеально не занимают пространства стека... С# уже может делать что-то вроде этого с ожиданием и выходом, но С++, возможно, придется перестроить, чтобы получить его.

concurrency в значительной степени сфокусирован на разделении проблем, когда проблема связана с задачей, которую программа должна завершить. это разделение проблем может быть достигнуто с помощью ряда средств... обычно это делегирование какого-то рода. идея concurrency заключается в том, что ряд процессов может выполняться независимо (разделение проблем), а "слушатель" направляет все, что вырабатывается этими отдельными проблемами, туда, куда он должен идти. это сильно зависит от какого-то асинхронного управления. Существует ряд подходов к concurrency, включая аспектно-ориентированное программирование и другие. С# имеет оператор "делегат", который работает довольно хорошо.

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