【问题标题】:What are coroutines in C++20?C++20 中的协程是什么?
【发布时间】:2017-09-16 03:37:33
【问题描述】:

中的协程是什么?

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

下图来自 ISOCPP。

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

【问题讨论】:

  • 回答“协程的概念与并行并发有何不同?” -- en.wikipedia.org/wiki/Coroutine
  • James McNellis 的演示文稿“C++ 协程简介”(Cppcon2016) 是一个很好且易于理解的协程介绍。
  • 最后,介绍一下“C++ 中的协程与其他语言的协程和可恢复函数的实现有何不同?”也很好。 (上面链接的维基百科文章与语言无关,没有解决)
  • 还有谁读过这个“C++20 中的隔离”?

标签: c++20 c++ coroutine c++20


【解决方案1】:

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

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

线程有多个“执行线程”和多个执行状态。你有不止一个程序和不止一个执行线程。

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


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

在伪代码中:

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

调用Generator,第一次调用它返回0。它的状态被记住(有多少状态随协同程序的实现而变化),下次调用它时,它会从中断的地方继续。所以它下次返回 1。然后 2.

最后到达循环的末尾,从函数的末尾掉下来;协程完成。 (此处发生的情况因我们所讨论的语言而异;在 python 中,它会引发异常)。

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

有两种协程;堆叠式和无堆叠式。

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

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

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

产生一个值的过程称为“yield”,因为协程有点像协作多线程;您正在将执行点交还给调用者。

Boost 有一个堆栈协程的实现;它可以让你调用一个函数来为你让步。堆栈式协程更强大,但也更昂贵。


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

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


C++中Coroutines的具体实现有点意思。

在最基本的层面上,它为 C++ 添加了一些关键字:co_returnco_awaitco_yield,以及一些与它们一起使用的库类型。

一个函数通过在其主体中包含其中一个而成为协程。所以从它们的声明来看,它们与函数没有区别。

当在函数体中使用这三个关键字之一时,会发生一些标准强制检查返回类型和参数,并且函数会转换为协程。这种检查告诉编译器在函数挂起时将函数状态存储在哪里。

最简单的协程是生成器:

generator<int> get_integers( int start=0, int step=1 ) {
  for (int current=start; true; current+= step)
    co_yield current;
}

co_yield 暂停函数执行,将该状态存储在generator&lt;int&gt; 中,然后通过generator&lt;int&gt; 返回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_lines 可能是异步协程,它们打开文件并从中读取行。 co_awaitload_data 的挂起和就绪状态与其进度联系起来。

C++ 协程比这更灵活,因为它们是作为用户空间类型之上的最小语言特性集实现的。用户空间类型有效地定义了 co_return co_awaitco_yield mean - 我见过人们使用它来实现单子可选表达式,例如 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;
}

【讨论】:

  • 这是我读过的关于协程是什么的最清晰的解释之一。将它们与 SIMD 和经典线程进行比较并将它们与 SIMD 和经典线程区分开来是一个绝妙的主意。
  • 我不明白 add-optionals 示例。 std::optional 不是等待对象。
  • @mord 是的,它应该返回 1 个元素。可能需要抛光;如果我们想要多行需要不同的控制流。
  • @l.f.抱歉,应该是;;
  • @L.F.对于这样简单的功能,也许没有区别。但我通常看到的不同之处在于,协程会记住其主体中的进入/退出(执行)点,而静态函数每次都从头开始执行。我猜“本地”数据的位置无关紧要。
【解决方案2】:

协程就像一个 C 函数,它有多个 return 语句,当第二次调用时,它不会在函数的开头开始执行,而是在前一次执行 return 之后的第一条指令处开始执行。此执行位置与所有将在非协程函数中存在于堆栈中的自动变量一起保存。

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

【讨论】:

  • 为什么是“像 C 函数”而不是“像函数”?
【解决方案3】:

协程应该是(在 C++ 中)能够“等待”某个其他例程完成并为挂起、暂停、等待的例程继续执行所需的任何功能的函数。 C++ 人最感兴趣的特性是协程在理想情况下不占用堆栈空间...C# 已经可以使用 await 和 yield 执行类似的操作,但 C++ 可能需要重新构建才能使用。

并发主要关注关注点的分离,其中关注点是程序应该完成的任务。这种关注点的分离可以通过多种方式实现……通常是某种委托。并发的想法是许多进程可以独立运行(关注点分离),并且“侦听器”会将这些分离的关注点产生的任何内容引导到它应该去的任何地方。这在很大程度上依赖于某种异步管理。有许多并发方法,包括面向方面的编程和其他方法。 C# 有'delegate' 操作符,效果很好。

并行性听起来像并发,可能会涉及,但实际上是一种物理构造,涉及以或多或少并行方式排列的许多处理器,软件能够将部分代码引导到将运行它的不同处理器,结果将同步接收。

【讨论】:

  • 并发和关注点分离完全无关。协程不是为暂停的例程提供信息,它们可恢复的例程。
猜你喜欢
  • 2020-07-26
  • 1970-01-01
  • 2020-01-10
  • 1970-01-01
  • 2020-01-22
  • 2020-10-01
  • 2021-09-03
  • 1970-01-01
相关资源
最近更新 更多