赞
踩
从2017年开始, 协程(Coroutine)的概念就开始被建议加入C++20的标准中了,并已经开始有人对C++20协程的提案进行了介绍。1事实上,协程的概念在很早就出现了,甚至其他语言(JS,Python,C#等)早就已经支持了协程。
可见,协程并不是C++所特有的概念。
简单来说,协程就是一种特殊的函数,它可以在函数执行到某个地方的时候暂停执行,返回给调用者或恢复者(可以有一个返回值),并允许随后从暂停的地方恢复继续执行。注意,这个暂停执行不是指将函数所在的线程暂停执行,而是单纯的暂停执行函数本身。
那么,这种特殊函数有什么用呢?最常见的用途,就是将“异步”风格的编程“同步”化。
比如,我们有一个请求webapi的库,然后在某个应用中我们需要发送一个http请求,然后等待web服务器反馈消息。恰巧的是,我们需要按顺序请求多次,比如,只有请求A返回了,我们才能发送请求B,因为请求B中包含请求A返回的结果。然后等请求B返回了,我们才能发送请求C等等。
我们不能阻塞主线程,那么此时我们应该怎么办?
最常见的思路就是开一个新线程,然后使用“回调函数”,例如:
// 示意代码
void requestA(int req, std::function<void(int)> cb)
{
// 我们的webapi是异步调用, 我们开启一个线程请求并等待调用完毕
std::thread t([req, cb]() {
auto response = webapi.request(req);
// 假定response有个等待返回值的接口waitForFinish,他会阻塞当前线程,直到拿到返回值
int rt = response.waitForFinish();
// 返回了, 那么我们调用回调函数
cb(rt);
});
t.detach();
}
假定我们还有相同结构的requestB,requestC以及其它, 那么我们会怎么用呢? 有了lamda表达式,通过回调函数进行链式调用可以很简单的写成如下形式:
int main() { requestA(1, [](int rt){ requestB(rt, [](int rt2){ requestC(rt2, [](int rt3){ // 根据需要可能会继续嵌套下去 }); }); }); // 甚至可能需要再来一遍, 因为我们还需要使用另一个参数请求 requestA(2, [](int rt){ requestB(rt, [](int rt2){ requestC(rt2, [](int rt3){ // 根据需要可能会继续嵌套下去 }); }); }); }
这还是好的,如果你使用Qt的信号槽来实现,并同时可能有多个请求,你可能还会遇到另一个问题:“我怎么知道这个返回值是我发送的哪个请求产生的?”如果webapi库没有提供请求与反馈之间互相对应的相关支持,你可能会更加的郁闷。
那么, 使用协程又会有哪些不一样呢?
想象一下, 同样的requestA,requestB,requestC,(当然已经修改为了协程的写法) 你可以这么用
task<void> request()
{
int rt = co_await requestA(1);
// 处理一些中间结果
rt = co_await requestB(rt);
// 处理一些中间结果
rt = co_await requestC(rt);
// 对最终结果做一些事情
}
这三个异步函数会在同一个线程中按照调用顺序依次完成调用。
没错, 不再需要回调函数, 你可以完全顺序的, 仿佛异步调用不存在的使用同步调用的写法。正是因为协程,我们就可以使用一个更加“同步”化的方式,实现异步调用了。
只要一个关键字co_await就能享用。隔壁的JavaScript早就用上了(ES6版本),现在,终于,C++也可以使用了!
很遗憾,并不能。
C++20的协程只是给了我们一个“使用同步风格进行异步调用”的框架,具体的实现还是需要我们自己去做。
如果你对JavaScript中的协程有所了解的话,就会明白,在ES6中,一个函数可以通过await等待返回的前提,是这个函数被声明为async,而这是ES6提供的一个“语法糖”,也就是说,这个关键字只起到“提示”的作用,真正的实现是需要Promise的。
C++20中也是这样,协程是特殊函数,但是在C++20中,这个特殊函数不是由普通函数添加一个关键字组成的,我们需要为实现这个特殊函数做一些额外的工作。
目前,C++20应该不会提供自动化的包装功能,或者简化包装的库,也就是说,想要让某个函数成为协程函数,我们需要人工的做一些额外的工作,一些辅助的自动化的工具应该会在C++23标准中提供,让协程真正的可以被广大开发人员使用。
虽然辅助工具再C++23才会提供,但是最基础的已经在C++20中存在了。
在我们继续讲解之前,先明确一些概念。
co_return,co_yield,co_await是为了使用协程而新增加的三个关键字,这些关键字在非协程函数中是无法使用的。这也就意味着,在main函数中直接调用co_await xxxx(); 是不行的。
这似乎有点违反我们的常识。协程的关键字只能在协程函数中使用有点递归的意思,这难道意味着普通的函数中没法使用协程函数了?这其实是我们一开始听说协程的描述时会产生的一种误解。
为了消除这种误解,我们先了解一下到底什么是协程函数,以及它到底特殊在哪里。
接下来我们先从如何定义协程函数开始:
简单来说,就是如果一个函数的返回值是一个符合Promise规范的类,并且在这个函数中使用了co_return,co_yield,co_await中的一个或多个,那么这个函数就是一个协程函数。
那么Promise规范又是啥?Promise在英文中是许诺的意思。简单来说,Promise规范就是:如果在类A中定义一个叫做promise_type的结构体,并且其中包含特定名字的函数,那么这个类A就符合Promise规范,它就是一个符合Promise规范的类,它也就是一个Promise。
比如以下例子:
struct task{
struct promise_type {
auto get_return_object() { return task{}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() { return {}; }
void return_void() {}
void unhandled_exception() {}
};
}
由于类task中定义了promise_type,同时其中包含了符合规范的5个函数,它就是一个Promise。
然后根据协程规范,返回这个类的函数就是协程函数,于是如果我们有以下定义:
task getTask() {
// 实现中不需要返回task,也不能写return
co_return;
}
getTask()就是一个协程函数了。当然,如果协程函数中不使用co_wait或者co_yield其实就没有什么意义。
然而,我们虽然有了协程函数,但是我们依旧无法使用co_await,为什么呢?因为co_await关键字实际上是一个运算符,其后面只能跟随一个“实现了三个特定函数的类”。这三个特定函数如下所述:2
struct suspend_always {
constexpr bool await_ready() const noexcept { return false; }
constexpr void await_suspend(std::coroutine_handle<> h) const noexcept {}
constexpr void await_resume() const noexcept {}
};
注意,我们实现的时候只需要有包含这三个名字的函数就行了,并不需要继承。
如果我们使用co_await suspend_always(); 会发生什么呢?
也就是说可以使用以下的模板类让co_wait的返回值更加的自由:3
template <class T>
struct someAsyncOpt {
bool await_ready()
void await_suspend(std::coroutine_handle<>);
T await_resume();
};
最后,我们也应该了解,同一个线程在一个时间点最多只能跑一个协程;在同一个线程中,协程的运行是穿行的,没有数据争用(data race),也不需要锁。
至此,我们完成了协程的基本介绍。
了解了协程后我们就可以发现了以下事实:
知道了以上事实,我们就可以按照以下方式使用协程了:
协程事实上并没有消灭回调函数,它只是为我们提供了一种方案,让我们可以“用同步调用的方式进行异步调用”。
回调函数还是存在的,只是被实现所隐藏起来了。
同时,协程并不是只能用于“用同步调用的方式进行异步调用”,它的本意其实就是“协同工作”。
也就是我等待你完成某个操作再去执行其它的操作,和多线程类似,但是避免了资源竞争,因为只有一个线程。
所有拥有类似需求的情况都可以使用协程来做。
目前C++20中协程只是刚刚出现,作为一个基础设施存在,因为缺乏必要的辅助支持的库,直接使用协程反而会增加开发的复杂度和困难度。我们可以等待C++23为我们带来一个更好用的协程,而现在我们需要的就是了解而已。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。