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


104

什么是协程

它与“ Parallelism2”或/和“ Concurrency2”有何不同(请看下图)?

下图来自ISOCPP。

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

在此处输入图片说明


3
回答“ 协程的概念与并行性并发性有何不同?” - en.wikipedia.org/wiki/Coroutine
本·福格特


3
James McNellis的演示文稿“ C ++协程简介”(Cppcon2016)是协程的一个很好且易于遵循的简介
philsumuru

2
最后,它也将是很好的掩护“是如何协同程序在C ++的其它语言的协同程序和可恢复函数的实现有什么不同?” (以上链接的维基百科文章(与语言无关)未解决)
Ben Voigt

1
还有谁读过这个“ C ++ 20隔离”?
Sahib Yar

Answers:


198

在抽象层次上,协程将具有执行状态的想法与具有执行线程的想法分开。

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

线程具有多个“执行线程”和多个执行状态。您拥有一个以上的程序,并具有一个以上的执行线程。

协程具有多个执行状态,但不拥有执行线程。您有一个程序,并且该程序具有状态,但是没有执行线程。


协程最简单的例子是其他语言的生成器或可枚举对象。

用伪代码:

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

Generator被调用,并在第一次调用时返回0。它的状态会被记住(多少状态会随着协程的实现而变化),下次您调用它时,它会从中断的地方继续。因此,下一次返回1。然后2。

最后,它到达循环的末尾并脱离函数的末尾。协程完成。(根据我们所讨论的语言,这里发生的事情会有所不同;在python中,它会引发异常)。

协程为C ++带来了这一功能。

协程有两种。一叠又一叠

无堆栈协程仅在其状态和执行位置中存储局部变量。

堆栈式协程存储整个堆栈(如线程)。

无堆栈协程的重量极轻。我阅读的最后一个建议基本上涉及将您的函数重写为类似lambda的形式。所有局部变量都进入对象的状态,并且使用标签从协程“产生”中间结果的位置跳入/跳出。

产生值的过程称为“收益”,因为协程有点像协作多线程。您将执行点交还给调用者。

Boost具有堆栈式协程的实现;它使您可以调用一个函数来为您屈服。堆栈式协程更强大,但也更昂贵。


协程不仅仅是一个简单的生成器。您可以在协程中等待协程,这可以使您以有用的方式编写协程。

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


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_resourceread_lines可能是异步协程,它们打开文件并从中读取行。该co_await的悬浮和准备状态时连接load_data到他们的进步。

C ++协程比这更加灵活,因为它们是作为用户空间类型之上的最少语言功能集实现的。用户空间类型有效地定义了含义co_return co_awaitco_yield 含义 -我已经看到人们使用它来实现一元可选表达式,这样co_await一个空的可选上的a 自动将空状态传播到外部的可选上:

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和经典线程区分开是一个好主意。
五花八门的

2
我不明白add-optionals示例。std :: optional <int>不是等待的对象。
吉夫·达德森

1
@mord是,它应该返回1个元素。可能需要抛光;如果我们要多条线,则需要不同的控制流程。
Yakk-Adam Nevraumont

1
@lf抱歉,应该是;;
Yakk-Adam Nevraumont

1
对于这样简单的功能,@ LF可能没有什么区别。但是我通常看到的区别是,协程会记住其主体中的进入/退出(执行)点,而静态函数每次都会从头开始执行。我猜“本地”数据的位置无关紧要。
AVP

21

协程就像一个具有多个return语句的C函数,并且在第二次调用时,不是在函数的开头而是在先前执行的返回之后的第一条指令处开始执行。该执行位置与所有在非协程函数中自动存在的变量一起保存。

Microsoft先前的实验协程实现确实使用了复制堆栈,因此您甚至可以从深层嵌套函数中返回。但是此版本被C ++委员会拒绝。您可以使用Boosts光纤库获得此实现。


1

协程应该是(在C ++中)能够“等待”其他例程完成并提供暂停,暂停,等待例程继续进行所需的任何功能。C ++人士最感兴趣的功能是协程在理想情况下不占用堆栈空间... C#可以通过await和yield来完成类似的事情,但可能必须重新构建C ++才能使用。

并发主要集中在关注点分离上,其中关注点是程序应该完成的任务。这种关注点分离可以通过多种方式来完成...通常是某种形式的委派。并发的思想是,多个流程可以独立运行(关注点分离),“监听器”会将那些分离的关注点产生的结果定向到应该去的任何地方。这在很大程度上取决于某种异步管理。有许多并发方法,包括面向方面的编程和其他方法。C#具有“ delegate”运算符,可以很好地运行。

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


9
并发和关注点分离是完全无关的。协程不提供暂停例程的信息,它们可恢复的例程。
Ben Voigt
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.