C + + 20中的协程是什么?

中的协程是什么?

它在哪些方面不同于“并行2”或/和“并发2”(请看下图) ?

下图来自 ISOCPP。

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

enter image description here

61930 次浏览

Coroutines 应该是(在 C + + 中)能够“等待”其他例程完成的函数,并为挂起的、暂停的、等待的、例程继续运行提供所需的任何东西。对于 C + + 用户来说,最有趣的特性是协程在理想情况下不会占用堆栈空间... ... C # 已经可以使用 wait 和屈服来做类似的事情,但是 C + + 可能需要重新构建才能使用它。

并发性主要集中在关注点分离上,其中一个问题是程序应该完成的任务。这个关注点分离可以通过多种方式来完成... 通常是某种授权。并发的概念是,许多进程可以独立运行(关注点分离) ,而“监听器”将把这些分离的关注点产生的任何东西引导到它应该去的地方。这在很大程度上依赖于某种异步管理。有许多并发方法,包括面向方面的编程和其他方法。C # 有一个非常好用的“委托”操作符。

并行性听起来像并发,可能涉及到,但实际上是一个物理结构,涉及到许多处理器,这些处理器或多或少地与软件并行排列,软件能够将部分代码导向不同的处理器,在这些处理器上运行代码,结果将被同步地接收回来。

在抽象层次上,Coroutines 将执行状态的概念与执行线程的概念分离开来。

SIMD (单指令流多数据流)有多个“执行线程”,但只有一个执行状态(它只能处理多个数据)。可以说并行算法有点像这样,因为您有一个“程序”运行在不同的数据上。

线程有多个“执行线程”和多个执行状态。您有多个程序和多个执行线程。

协同程序具有多个执行状态,但不拥有执行线程。你有一个程序,程序有状态,但是它没有执行线程。


协同程序最简单的例子是来自其他语言的生成器或可枚举数。

在伪代码中:

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

调用 Generator,第一次调用时返回 0。它的状态会被记住(协程的实现会改变多少状态) ,下次调用它时,它会继续它停止的地方。所以下次返回1。然后是2。

最后,它到达循环的末尾并脱离函数的末尾; 协程完成。(这里发生的情况因我们讨论的语言而异; 在 python 中,它抛出一个异常)。

协同程序为 C + + 带来了这种能力。

协程有两种类型: 堆栈式和无堆栈式。

无栈协同程序只存储其状态和执行位置的局部变量。

一个堆栈协程存储整个堆栈(像一个线程)。

无堆栈协同程序可以是非常轻的重量。我读到的最后一个方案基本上是把你的函数重写成一个有点像 lambda 的东西; 所有的局部变量都进入一个对象的状态,标签被用来跳转到/从协程“产生”中间结果的位置。

生成值的过程称为“屈服”,因为协程有点像合作多线程; 您将执行点返回给调用者。

Boost 有一个堆栈协程的实现; 它允许您调用一个函数来为您生成。堆叠式协程更强大,但也更昂贵。


协同程序不仅仅是一个简单的生成器。您可以在协程中等待一个协程,它允许您以一种有用的方式组合协程。

协同程序,比如 if、循环和函数调用,是另一种“结构化 goto”,它允许您以更自然的方式表达某些有用的模式(比如状态机)。


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>中,然后通过 generator<int>返回 current的值。

可以对返回的整数进行循环。

同时,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_resourceread_line可能是打开文件并从中读取行的异步协同程序。co_awaitload_data的暂停和就绪状态与它们的进度连接起来。

C + + 协同程序比这更加灵活,因为它们是作为用户空间类型之上的一组最小的语言特性来实现的。用户空间类型有效地定义了什么是 co_return co_awaitco_yield 刻薄——我见过人们用它来实现一元可选表达式,比如空可选表达式上的 co_await会自动将空状态传播到外部可选表达式:

modified_optional<int> add( modified_optional<int> a, modified_optional<int> b ) {
co_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 函数,它有多个返回语句,当第二次调用时,并不是在函数开始时开始执行,而是在前一次执行的返回之后的第一条指令开始执行。这个执行位置与所有自动变量一起保存在非协同函数的堆栈中。

微软以前的一个实验性协同程序实现确实使用了复制的堆栈,因此您甚至可以从深度嵌套的函数返回。但是这个版本被 C + + 委员会否决了。例如,可以使用 Boosts 光纤库获得此实现。