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

139

3
“协程”这一概念与“并行”和“并发”有何不同之处? - Ben Voigt
相关链接:https://dev59.com/i5Pfa4cB1Zd3GeqPFZo8 - Ben Voigt
4
有一个非常好而且易于理解的协程介绍,是James McNellis在Cppcon2016上做的演讲“Introduction to C++ Coroutines”。 - philsumuru
2
最后,涵盖“C++中的协程与其他语言实现协程和可恢复函数的区别是什么?”也是很好的(上面链接的维基百科文章不涉及这个问题,因为它是与语言无关的)。 - Ben Voigt
1
还有谁读过这篇关于 C++20 中的“隔离”(quarantine)的文章? - Sahib Yar
2
James McNellis在CppCon 2016上的“C++协程介绍”YouTube链接:https://youtu.be/ZTqHjjm86Bw - Milan
3个回答

268

从抽象的层面来看,协程将执行状态与执行线程的概念分离。

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

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

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


最简单的协程示例是其他语言中的生成器或枚举。

伪代码示例:

function Generator() {
  for (i = 0 to 100)
    produce i
}
Generator被调用,第一次调用返回0。其状态会被记住(随着协程实现的不同而变化),下一次调用它将从上次离开的地方继续执行,所以它下一次返回1,然后是2,直到循环结束时函数退出,协程结束。关于在此发生的情况因我们讨论的语言而异,在Python中会抛出异常。协程为C++带来了这种功能。有两种类型的协程; stackful和stackless。Stackless coroutine只在其状态和执行位置中存储局部变量。Stackful coroutine存储整个堆栈(类似于线程)。Stackless coroutines可能非常轻量级。我读过的最后一个提议基本上涉及将您的函数重写为类似于lambda的东西;所有局部变量都进入对象的状态中,使用标签跳转到/从协程“产生”中间结果的位置。生成值的过程称为“yield”,因为协程有点像合作式多线程; 将执行点返回到调用者。Boost具有stackful协程的实现;它允许您调用一个函数来替您进行yield。Stackful协程更为强大,但也更昂贵。
协程与简单生成器不同,您可以在协程中等待另一个协程,这样可以有用地组合协程。协程类似于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>中,并通过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;
}

38
这是我曾经读过的对协程解释最清晰的之一。将它们与SIMD和传统线程进行比较并区分开来是一个很好的想法。 - Omnifarious
3
我不理解 add-optionals 示例。std::optional<int> 不是一个可等待对象。 - Jive Dadson
1
@l.f. 抱歉,应该是 ;; - Yakk - Adam Nevraumont
1
@L.F. 对于这样简单的函数,可能没有什么区别。但是我通常看到的区别是,协程会记住其体内的入口/出口(执行)点,而静态函数每次都从开头开始执行。我想“本地”数据的位置是无关紧要的。 - avp
1
@huseyintugrulbuyukisik 不行。 - Yakk - Adam Nevraumont
显示剩余12条评论

23

协程就像一个C函数,它有多个返回语句,并且当第二次调用时不会从函数开始执行,而是从上一次执行的返回后的第一条指令开始执行。这个执行位置与在非协程函数中将存储在堆栈中的所有自动变量一起保存。

微软先前实验性地实现了一个协程版本,它使用了复制的堆栈,所以你甚至可以从深层嵌套的函数中返回。但是这个版本被C++委员会拒绝了。例如,您可以使用Boosts fiber库获取此实现。


2
为什么说它“像C函数”而不是“像函数”? - einpoklum

0

协程(coroutines)是指(在C++中)能够“等待”其他程序完成并提供所需内容以使暂停、暂停等待的程序继续执行的函数。对于C++开发人员最感兴趣的特性是,协程理想情况下不会占用堆栈空间... C#已经可以使用await和yield实现类似的功能,但C++可能需要重新构建才能实现。

并发(concurrency)主要关注程序应该完成的任务,其中一个任务被称为一个关注点。这种关注点的分离可以通过多种方式来实现...通常是通过委托来实现。并发的思想是让多个进程独立运行(关注点的分离),并且“监听器”将由这些分离的关注点产生的任何内容定向到其应该去的地方。这在很大程度上依赖于某种异步管理。并发有许多方法,包括面向方面编程等。C#具有“委托”运算符,可以很好地工作。

并行(parallelism)听起来像并发,并且可能涉及,但实际上它是一个物理结构,涉及许多处理器以更或多或少并行的方式排列,配合软件将代码的部分指向不同的处理器,其中将被运行并接收结果。


12
并发和关注点分离是完全无关的。协程的作用不是为挂起的例程提供信息,而是作为可恢复例程本身存在。 - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接