Что такое сопрограммы в c++20?
Чем он отличается от "Parallelism2" или/и "Concurrency2" (см. Изображение ниже)?
Изображение ниже от ISOCPP.
Что такое сопрограммы в c++20?
Чем он отличается от "Parallelism2" или/и "Concurrency2" (см. Изображение ниже)?
Изображение ниже от ISOCPP.
На абстрактном уровне 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;
}
Корутин похож на функцию C, которая имеет несколько операторов возврата, а при вызове второго раза не запускает выполнение в начале функции, а в первой команде после предыдущего выполненного возврата. Это место выполнения сохраняется вместе со всеми автоматическими переменными, которые будут жить в стеке в не сопроцессорных функциях.
Предыдущая экспериментальная реализация coroutine от Microsoft использовала скопированные стеки, чтобы вы могли даже вернуться из глубоких вложенных функций. Но эта версия была отвергнута комитетом С++. Вы можете получить эту реализацию, например, с библиотекой волокон Boosts.
Предполагается, что сопрограммы должны быть (в С++) функциями, которые могут "ждать" для выполнения какой-либо другой процедуры и предоставлять все необходимое для приостановленной, приостановленной, ожидающей, подпрограммы. функция, которая наиболее интересна для людей С++, заключается в том, что сопрограммы идеально не занимают пространства стека... С# уже может делать что-то вроде этого с ожиданием и выходом, но С++, возможно, придется перестроить, чтобы получить его.
concurrency в значительной степени сфокусирован на разделении проблем, когда проблема связана с задачей, которую программа должна завершить. это разделение проблем может быть достигнуто с помощью ряда средств... обычно это делегирование какого-то рода. идея concurrency заключается в том, что ряд процессов может выполняться независимо (разделение проблем), а "слушатель" направляет все, что вырабатывается этими отдельными проблемами, туда, куда он должен идти. это сильно зависит от какого-то асинхронного управления. Существует ряд подходов к concurrency, включая аспектно-ориентированное программирование и другие. С# имеет оператор "делегат", который работает довольно хорошо.
parallelism звучит как concurrency и может быть задействован, но на самом деле представляет собой физическую конструкцию с участием многих процессоров, расположенных более или менее параллельно с программным обеспечением, которое может направлять части кода на разные процессоры, где он будет запущен и результаты будут получены синхронно.