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


104

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

Чем он отличается от «Parallelism2» и / или «Concurrency2» (посмотрите на изображение ниже)?

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

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

введите описание изображения здесь


3
Чтобы ответить: «Чем концепция сопрограмм отличается от параллелизма и параллелизма ?» - en.wikipedia.org/wiki/Coroutine
Бен Фойгт,


3
Очень хорошее и понятное введение в сопрограммы - это презентация Джеймса Макнеллиса «Введение в сопрограммы C ++» (Cppcon2016)
philsumuru

2
Наконец, было бы неплохо затронуть тему «Чем сопрограммы в C ++ отличаются от реализаций сопрограмм и возобновляемых функций в других языках?» (который не рассматривается в указанной выше статье в Википедии, будучи независимым от языка)
Бен Фойгт,

1
кто еще читал этот "карантин в C ++ 20"?
Сахиб Яр,

Ответы:


203

На абстрактном уровне сопрограммы отделяют идею состояния выполнения от идеи наличия потока выполнения.

SIMD (одна инструкция с несколькими данными) имеет несколько «потоков выполнения», но только одно состояние выполнения (он просто работает с несколькими данными). Возможно, параллельные алгоритмы немного похожи на это в том смысле, что у вас есть одна «программа», работающая с разными данными.

У потоковой передачи есть несколько «потоков выполнения» и несколько состояний выполнения. У вас есть более одной программы и более одного потока выполнения.

Сопрограммы имеют несколько состояний выполнения, но не владеют потоком выполнения. У вас есть программа, и у нее есть состояние, но у нее нет потока выполнения.


Самый простой пример сопрограмм - это генераторы или перечисления из других языков.

В псевдокоде:

function Generator() {
  for (i = 0 to 100)
    produce i
}

В GeneratorНазывается, и первый раз , когда он назвал ее возвращает 0. Его состояние запоминается (насколько состояние меняется в зависимости от реализации сопрограмм), и в следующий раз, когда вы его вызываете, оно продолжается с того места, где было остановлено. Так что в следующий раз он вернет 1. Потом 2.

Наконец, он достигает конца цикла и падает с конца функции; сопрограмма готова. (То, что здесь происходит, зависит от языка, о котором мы говорим; в python это вызывает исключение).

Сопрограммы привносят эту возможность в C ++.

Есть два вида сопрограмм; стопки и без стопки.

Бестековая сопрограмма хранит только локальные переменные в своем состоянии и месте выполнения.

В стековой сопрограмме хранится весь стек (например, поток).

Бестековые сопрограммы могут быть очень легкими. Последнее предложение, которое я прочитал, касалось в основном переписывания вашей функции во что-то вроде лямбды; все локальные переменные переходят в состояние объекта, а метки используются для перехода в / из места, где сопрограмма «производит» промежуточные результаты.

Процесс создания значения называется «yield», поскольку сопрограммы немного похожи на кооперативную многопоточность; вы возвращаете точку исполнения вызывающему.

Boost имеет реализацию стековых сопрограмм; он позволяет вам вызывать функцию, которая будет уступать вам. Стековые сопрограммы более мощные, но и более дорогие.


Сопрограммы - это больше, чем простой генератор. Вы можете ожидать сопрограмму в сопрограмме, что позволяет вам составлять сопрограммы удобным способом.

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


Конкретная реализация Coroutines на 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в ней. Если они готовы, приступайте немедленно; в противном случае вы приостанавливаете работу до тех пор, пока ожидаемый объект не будет готов.

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_lines, вероятно, являются асинхронными сопрограммами, открывающими файл и считывающими из него строки. co_awaitСоединяет Приостановка и состояние готовности load_dataк их прогрессу.

Сопрограммы C ++ гораздо более гибкие, чем это, поскольку они были реализованы как минимальный набор языковых функций поверх типов пользовательского пространства. Типы пользовательского пространства эффективно определяют, что co_return co_awaitи co_yield означают - я видел, как люди использовали его для реализации монадических необязательных выражений, таких как a 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;
}

26
Это одно из самых ясных объяснений того, что такое сопрограммы, которое я когда-либо читал. Сравнивать их и отличать от SIMD и классических нитей было отличной идеей.
Omnifarious

2
Я не понимаю пример дополнительных опций. std :: optional <int> не является ожидаемым объектом.
Джайв Дадсон

1
@mord да он должен возвращать 1 элемент. Может потребоваться полировка; если нам нужно более одной строки, нужен другой поток управления.
Якк - Адам Неврамонт

1
@lf извини, должно было быть ;;.
Yakk - Adam Nevraumont

1
@LF для такой простой функции может и нет разницы. Но разница, которую я вижу в целом, заключается в том, что сопрограмма запоминает точку входа / выхода (выполнения) в своем теле, тогда как статическая функция каждый раз запускает выполнение с начала. Полагаю, расположение "локальных" данных не имеет значения.
avp

21

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

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


1

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

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

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


9
Параллелизм и разделение ответственности совершенно не связаны. Сопрограммы не предоставляют информацию для приостановленной процедуры, они являются возобновляемыми подпрограммами.
Бен Фойгт
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.