IE盒子

搜索
查看: 175|回复: 20

The Coroutine in C++ 20 协程初探

[复制链接]

3

主题

11

帖子

21

积分

新手上路

Rank: 1

积分
21
发表于 2023-2-7 16:09:15 | 显示全部楼层 |阅读模式
C++已经从C++11演化到了C++20,你还在用C++98吗?
引言

本文将唠唠C++ 20的coroutine(协程),如果对coroutine,async, await不了解的,可以先移步阅读Coroutine, 异步,同步,async, await——
Coroutine就是广义上的函数,只不过是可以suspend和resume的函数,也就是你可以暂停这个函数的执行(实际上就是在suspend的地方直接返回到caller了),去做其他事情,然后在恰当的时候恢复到你离开的位置继续开始运行。
如下图左边灰色的coroutine(特别点的函数),当它被调用的时候,被切分成了两个部分,在线程1执行完成第一部分后,就继续做其他事情了,第二部分被suspend了。在适当的时机出现后,线程2(可以是线程1)开始执行第二部分。


把一个完整的函数切分成多个部分,有什么好处呢?请看Coroutine, 异步,同步,async, await。本文关注C++是如何实现这样的效果~
JavaScript里面的coroutine使用起来很方便,长这个样子
async reply() {
  get_name(); //normal function call
  data = await read_data();
  res = await write(data);
  return res;
}
它们长得跟同步的代码很像,只是在block的函数调用前面添加了await关键字,而函数本身reply()标注了async关键字。它们执行的逻辑顺序不变(也就是按照代码书写的顺序执行)但是实际是异步执行的。具体可以参见Coroutine, 异步,同步,async, await。
C++的coroutine长什么样子呢?
Coroutine in C++

C++虽然是接近底层的编程语言,但C++ coroutine代码长得也差不多,如下
sync<int> reply() {
  get_name(); // normal function call
  std::string data = co_await read_data();
  int res = co_await write(data);
  return res;
}
可是要让上面的代码成功编译,不是一件简单的事情。这里先抛出能让它编译成功的完整的代码,可以暂时忽略具体的细节,感兴趣可以用VS2015/2019, Clang/GCC 10.0去尝试编译一下。发现太长了,我把它单独放到这里:A Simple C++ Coroutine
代码我加了Trace,方便跟踪程序的执行顺序。如果点进去随便瞄一下,会发现代码很长,第一次接触肯定一脸o((⊙﹏⊙))o 。下面具体分解。
首先reply()函数就叫coroutine,也就是它可以suspend,然后resume。那什么支持了它这么灵活呢?
大体上两个方面,一个编译器的支持,也就是完整的代码(指这里的代码:A Simple C++ Coroutine,下文意同)需要比较新的C++编译器才能编译;另一个方面是程序员按照跟编译器的约定编写特定的代码。
把函数变成Coroutine

将本文要讲解的代码从完整的代码摘出来如下:
template<typename T>
class lazy {
    bool await_ready()
    {
        const auto ready = this->coro.done();
        Trace t;
        std::cout << "Await " << (ready ? "is ready" : "isn't ready") << std::endl;
        return this->coro.done();
    }
    void await_suspend(std::experimental::coroutine_handle<> awaiting)
    {
        {
            Trace t;
            std::cout << "About to resume the lazy" << std::endl;
            this->coro.resume();
        }
        Trace t;
        std::cout << "About to resume the awaiter" << std::endl;
        awaiting.resume();
    }
    auto await_resume()
    {
        const auto r = this->coro.promise().value;
        Trace t;
        std::cout << "Await value is returned: " << r << std::endl;
        return r;
    }
}
lazy<std::string> read_data()
{
    Trace t;
    std::cout << "Reading data..." << std::endl;
    co_return "billion$!";
}

lazy<int> write_data()
{
    Trace t;
    std::cout << "Write data..." << std::endl;
    co_return 42;
}

sync<int> reply(int i)
{
    std::cout << "Started await_answer" << std::endl;
    auto a = co_await read_data();
    std::cout << "Data we got is " << a  << std::endl;
    auto v = co_await write_data();
    std::cout << "write result is " << v << std::endl;
    co_return 42;
}

因为reply()函数里面有co_await,要包装成coroutine,所以我们要sync<int>支持特定的接口,具体介绍放在单独的文章:The Coroutine in C++ 20 协程之诺。
而read_data()和write_data()被co_await了,我们需要它们的返回的lazy<std::string>支持下面三个接口

  • bool await_ready()
  • auto await_suspend(HandleType awaiting)
  • auto await_resume()
其中 using HandleType = std::experimental::coroutine_handle<>有了这三个接口,当reply() coroutine 进行co_await需要suspend的时候,await_suspend就被调用;当函数resume的时候,await_resume()就会被调用。
而函数什么时候会被suspend呢?当await_ready()返回false的时候,所以程序员需要在await_ready里面定义该不该suspend, 如:
bool await_ready()
    {
        const auto ready = this->coro.done();
        Trace t;
        std::cout << "Await " << (ready ? "is ready" : "isn't ready") << std::endl;
        return this->coro.done();
    }
当把函数suspend的时候,什么时候reply()会被resume呢?程序员需要在await_suspend()里面编写相应的调度代码。比如完整的代码里面,我们就马上resume了:
    void await_suspend(std::experimental::coroutine_handle<> awaiting)
    {
        //...省略部分代码
        Trace t;
        std::cout << "About to resume the reply()" << std::endl;
        awaiting.resume();
    }
实际的产品的代码会将coroutine (reply())放到到事件调度器,比如libuv(NodeJS的时间循环,我正在利用C++ 20 Coroutine使得libuv支持co_await,后续会讲解如何实现)。
而怎么resume呢?很简单,await_suspend()的传入参数就是一个reply() coroutine的handle,我们直接awaiting.resume()就将reply() resume了,见上面的代码。
是不是很神奇?
所以这三个接口的功能是:
1 await_ready()示意被co_await 的对象(read_data(),write_data())要不要将当前的coroutine (reply) suspend(挂起)。
2 await_suspend()负责当挂起的时候,定义什么时候可以被resume。
3 await_resume()定义当被 co_await的对象resume时要将什么作为co_await的返回值。
为什么会这么”神奇“

完整的代码read_data()和write_data()返回的lazy<std::string>还需要一些工作,比如initial_suspend, final_suspend,get_return_object等等,The Coroutine in C++ 20 协程之诺将继续讲解~
附注

Coroutine关键词有await, yield, async。在JavaScript里面一般不叫coroutine,叫async call或者Promise。
C++ 对应有co_await,co_yield, 还多了个co_return。async对应什么呢?请看我的续篇The Coroutine in C++ 20 协程之诺
参考文章:

  • C++ Coroutines: Understanding operator co_await
  • Awaiting
回复

使用道具 举报

1

主题

5

帖子

5

积分

新手上路

Rank: 1

积分
5
发表于 2023-2-7 16:09:44 | 显示全部楼层
把GCC 10.0支持的相关字样去掉吧!
BUG实在太多,多到我都不想吐槽。
另,作为C++ cotoutines提案的提出者,第一个实现者,第一个完整实现者,还是把VS加上吧。VS2015到VS2019都可以的。
回复

使用道具 举报

3

主题

10

帖子

16

积分

新手上路

Rank: 1

积分
16
发表于 2023-2-7 16:10:33 | 显示全部楼层
已改。可否将你遇到的问题分享一下~
回复

使用道具 举报

1

主题

6

帖子

8

积分

新手上路

Rank: 1

积分
8
发表于 2023-2-7 16:10:55 | 显示全部楼层
https://github.com/tearshark/librf/blob/master/tutorial/gcc_bugs.cpp

这些都可以忍受。另外有一个BUG不太容易复现,会导致librf的resumable_main_event()测试陷入死循环。通过查看反编译的汇编代码,发现GCC编译出了错误的汇编结果。

另外,GCC还缺少一些coroutine的额外的函数,导致无法进行进一步优化。当然,这些额外的函数并不是C++ coroutine标准要求的。

再另外,__builtin_coro_frame()这个函数,要是再提供一个C++版本就好了。现在返回void*,导致无法推断本函数的promise类型,或者说无法推断本函数的返回类型,要走非常多的弯路,才能勉强达到同样的效果.
回复

使用道具 举报

3

主题

13

帖子

23

积分

新手上路

Rank: 1

积分
23
发表于 2023-2-7 16:11:41 | 显示全部楼层
多谢分享!有时间学习学习你写的这个库。
回复

使用道具 举报

3

主题

6

帖子

12

积分

新手上路

Rank: 1

积分
12
发表于 2023-2-7 16:11:52 | 显示全部楼层
万一以后标准出到89,到底哪个算新呢?89还是98呀
回复

使用道具 举报

1

主题

7

帖子

13

积分

新手上路

Rank: 1

积分
13
发表于 2023-2-7 16:12:19 | 显示全部楼层
哈哈哈~天道有轮回嘛
回复

使用道具 举报

0

主题

5

帖子

0

积分

新手上路

Rank: 1

积分
0
发表于 2023-2-7 16:13:02 | 显示全部楼层
到时候可能把年份补全了,就像js那边es5,ea6到现在es2018,es2020
回复

使用道具 举报

1

主题

2

帖子

3

积分

新手上路

Rank: 1

积分
3
发表于 2023-2-7 16:13:28 | 显示全部楼层
可惜现在只有语言支持,没有标准库支持
回复

使用道具 举报

2

主题

5

帖子

9

积分

新手上路

Rank: 1

积分
9
发表于 2023-2-7 16:13:43 | 显示全部楼层
c++本来就快的爆表,如果还有协程支持,那得多快
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表