第33章 协程(C++20)
33.1 协程的使用方法
#include <iostream>
#include <chrono>
#include <future>
using namespace std::chrono_literals;
std::future<int> foo()
{
std::cout << "call foo\n";
std::this_thread::sleep_for(3s);
co_return 5;
}
std::future<std::future<int>> bar()
{
std::cout << "call bar\n";
std::cout << "before foo\n";
auto n = co_await std::async(foo); // 挂起点
std::cout << "after foo\n";
co_return n;
}
int main()
{
std::cout << "before bar\n";
auto i = bar();
std::cout << "after bar\n";
i.wait();
std::cout << "result = " << i.get().get();
}
输出结果:
before bar
call bar
before foo
after bar
call foo
after foo
result = 5
线程执行到auto n = co_awaitstd::async (foo);之后跳出了bar函数,如同从函数中间return了一般,返回后执行了std::cout << “afterbar\n”;,最后等待std::future<std::future>的结果。
在理解co_await和co_return挂起和恢复协程之后,我们再来讨论另一种情况:
#include <iostream>
#include <experimental/generator>
std::experimental::generator<int> foo()
{
std::cout << "begin" << std::endl;
for (int i = 0; i < 10; i++) {
co_yield i;
}
std::cout << "end" << std::endl;
}
int main()
{
for (auto i : foo()) {
std::cout << i << std::endl;
}
}
输出结果:
begin
0
1
2
3
4
5
6
7
8
9
end
协程虽然提供了一种异步代码的编写方法,但是并不会自动执行异步操作,例如:
#include <iostream>
#include <chrono>
#include <future>
using namespace std::chrono_literals;
std::packaged_task<int()> task(
[]() {
std::cout << "call task\n";
std::this_thread::sleep_for(3s);
return 5;
}
);
std::future<int> bar()
{
return task.get_future();
}
std::future<void> foo()
{
std::cout << "call foo\n";
std::cout << "before bar\n";
auto i = co_await bar();
std::cout << "after bar\n";
std::cout << "result = " << i;
}
int main()
{
std::cout << "before foo\n";
auto w = foo();
std::cout << "after foo\n";
w.wait();
}
输出结果:
before foo
call foo
before bar
after foo
在上面的代码中,虽然使用auto i = co_await bar();挂起了协程,但是并没有其他线程执行异步操作,造成的结果就是w.wait();一直等待。
修改上面代码的bar函数:
std::future<int> bar()
{
std::future<int> r = task.get_future();
std::thread t(std::move(task));
t.detach();
return r;
}
输出结果:
before foo
call foo
before bar
after foo
call task
after bar
result = 5
33.2 协程的实现原理
33.2.1 co_await运算符原理
co_await运算符可以创建一个挂起点将协程挂起并等待协程恢复。
auto n = co_await std::async(foo);
可以拆解为:
std::future<std::future<int>> expr = std::async(foo);
auto n = co_await expr;
我们将表达式expr命名为可等待体,顾名思义是指该对象是可以被等待的。
并非所有对象都是可等待体
co_await std::string{ "hello" };
error C3312: no callable 'await_resume' function found for type 'std::string'
error C3312: no callable 'await_ready' function found for type 'std::string'
error C3312: no callable 'await_suspend' function found for type 'std::string'
目标对象可被等待需要实现await_resume、await_ready和await_suspen这3个成员函数。具备这3个函数的对象可以称为等待器,也就是说等待器和可等待体可以是同一个对象。
1.await_ready函数叫作is_ready或许更加容易理解,该函数用于判定可等待体是否已经准备好,也就是说可等待体是否已经完成了目标任务,如果已经完成,则返回true;否则返回false。
2.await_suspend这个函数名则更加令人难以理解,命名为schedule_ continuation应该会更加清晰,它的作用就是调度协程的执行流程,比如异步等待可等待体的结果、恢复协程以及将执行的控制权返回调用者。
3.await_resume实际上用于接收异步执行结果,可以叫作retrieve_value。
class awaitable_string : public std::string {
public:
using std::string::string;
bool await_ready() const { return true; }
void await_suspend(std::experimental::coroutine_handle<> h) const {}
std::string await_resume() const { return *this; }
};
std::future<std::string> foo()
{
auto str = co_await awaitable_string{ "hello" };
co_return str;
}
int main()
{
auto s = foo();
std::cout << s.get();
}
1.bool await_ready()返回true表明目标对象已经准备好了,也就是说协程无须在此挂起,执行流会继续按照代码编写顺序同步执行后续代码,在这种情况下await_suspend会被忽略,直接执行await_resume函数获得结果。如果函数返回false,则标识目标对象没有准备好,需要执行后续操作。
2.所谓的后续操作即调用void await_suspend(std::experimental:: coroutine_handle <> h)函数,这里有一个特殊的形参coroutine_handle<>,正如它的类型名所示,它是协程的句柄,可以用于控制协程的运行流程。读者不必了解其细节,只需要知道该句柄由编译器生成,其中包含协程挂起和恢复的上下文信息即可,coroutine_ handle<>有operator()和resume()函数,它们可以执行挂起点之后的代码。回到await_suspend函数本身,它可以借助coroutine_handle<>控制协程的执行流程。值得注意的是,
await_suspend不一定返回void类型,还可以返回bool和coroutine_handle类型。
1)返回void类型表示协程需要将执行流的控制权交给调用者,协程保持挂起状态。
2)返回bool类型则又会出现两种情况,当返回值为true时,效果和返回类型与void相同;当返回false的时候,则恢复当前协程运行。
3)返回coroutine_handle类型的时候,则会恢复该句柄对应的协程。
值得注意的是,如果在await_suspend中捕获到了异常,那么协程也会恢复并且在协程中抛出该异常。
3.std::string await_resume()实际上和恢复本身没有关系,可以看到它只是返回最终结果而已。
1.co_await运算符的重载
我们可以重载co_await运算符,让它从可等待体转换为等待器
awaitable_string operator co_await(std::string&& str)
{
return awaitable_string{ str };
}
std::future<std::string> foo()
{
auto str = co_await std::string{ "hello" };
co_return str;
}
2.可等待体和等待器的完整实现
最后让我们实现一个完整的可等待体和等待器来结束co_await的讨论:
#include <iostream>
#include <fstream>
#include <streambuf>
#include <future>
class file_io_string {
public:
file_io_string(const char* file_name) {
t_ = std::thread{ [file_name, this]() mutable {
std::ifstream f(file_name);
std::string str((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
result_ = str;
ready_ = true;
} };
}
bool await_ready() const { return ready_; }
void await_suspend(std::experimental::coroutine_handle<> h) {
std::thread r{ [h, t = std::move(t_)] () mutable {
t.join();
h(); }
};
r.detach();
}
std::string await_resume() const { return result_; }
private:
bool ready_ = false;
std::thread t_;
std::string result_;
};
std::future<std::string> foo()
{
auto str = co_await file_io_string{ "test.txt" };
co_return str;
}
int main()
{
auto s = foo();
std::cout << s.get();
}
file_io_string在构造函数中创建新线程执行文件读取操作并且设置ready_为true。一般情况下,主线程的执行会比IO线程快,所以主线程调用await_ready的时候ready_更可能为false,这时代码会执行await_suspend函数,await_suspend函数创建新线程等待文件IO线程执行完毕,并且从挂起点恢复执行foo函数。
33.2.2 co_yield运算符原理
struct my_int_generator {};
my_int_generator foo()
{
for (int i = 0; i < 10; i++) {
co_yield i;
}
}
error C2039: 'promise_type': is not a member of 'std::experimental::coroutine_traits<my_int_generator>'
promise_type
promise_type可以用于自定义协程自身行为,代码的编写者可以自定义协程的多种状态以及自定义协程中任何co_await、co_return或co_yield表达式的行为,比如挂起前和恢复后的处理、如何返回最终结果等。
C++标准提供了一种方式获取promise_type,那就是std::experimental::coroutine_traits。
想要实现一个generator可用的promise_type,有几个成员函数是必须实现的:
#include <experimental/resumable>
using namespace std::experimental;
struct my_int_generator {
struct promise_type {
int* value_ = nullptr;
my_int_generator get_return_object() {
return my_int_generator{ *this };
}
auto initial_suspend() const noexcept {
return suspend_always{};
}
auto final_suspend() const noexcept {
return suspend_always{};
}
auto yield_value(int& value) {
value_ = &value;
return suspend_always{};
}
void return_void() {}
};
explicit my_int_generator(promise_type& p)
: handle_(coroutine_handle<promise_type>::from_promise(p))
{}
~my_int_generator() {
if (handle_) {
handle_.destroy();
}
}
coroutine_handle<promise_type> handle_;
};
my_int_generator foo()
{
for (int i = 0; i < 10; i++) {
co_yield i;
}
}
int main()
{
auto obj = foo();
}
1.get_return_object是一个非常关键的函数,理解这个函数名我们需要从调用者的角度来看问题。可以看到调用者是main函数,它使用obj接受foo()执行的返回值。那么问题来了,foo()函数并没有return任何值。这时协程需要promise_type帮助它返回一个对象,这个辅助函数就是get_return_object。现在就好理解了,get_return_object就是通过my_int_generator的构造函数创建了一个对象并且返回给调用者,其中构造函数的形参接受一个promise_type的引用类型,并将其转换为coroutine_handle<promise_ type>类型。前文已经讨论过,coroutine_handle的作用是控制协程执行流,这里也不例外,我们后面需要用它来恢复协程的执行。
2.通常情况下我们不需要在意initial_suspend和final_suspend这两个函数,它们是C++标准给予代码库编写者在协程执行前后的挂起机会,程序员可以利用这些机会做一些额外的逻辑处理,大多数情况下是用不到的。值得注意的是,这两个函数的返回类型必须是一个等代器,为了代码编写的方便,标准为我们准备了两种等待器suspend_always和suspend_never,分别表示必然挂起和从不挂起
3.yield_value的意思很简单,保存co_yield操作数的值并且返回等待器,generator通常返回suspend_always。
4.return_void用于实现没有co_return的情况。promise_type中必须存在return_void或者return_value。
33.2.3 co_return运算符原理
和co_yield相同,co_return也需要promise_type的支持
struct my_int_return {
struct promise_type {
int value_ = 0;
my_int_return get_return_object() {
return my_int_return{ *this };
}
auto initial_suspend() const noexcept {
return suspend_never{};
}
auto final_suspend() const noexcept {
return suspend_always{};
}
void return_value(int value) {
value_ = value;
}
};
explicit my_int_return(promise_type& p)
: handle_(coroutine_handle<promise_type>::from_promise(p))
{}
~my_int_return() {
if (handle_) {
handle_.destroy();
}
}
int get() {
if (!ready_) {
value_ = handle_.promise().value_;
ready_ = true;
if (handle_.done()) {
handle_.destroy();
handle_ = nullptr;
}
}
return value_;
}
coroutine_handle<promise_type> handle_;
int value_ = 0;
bool ready_ = false;
};
my_int_return foo()
{
co_return 5;
}
int main()
{
auto obj = foo();
std::cout << obj.get();
std::cout << obj.get();
std::cout << obj.get();
}
如果co_return没有任何返回值,则需要用成员函数void return_void()代替void return_value(int value)。
33.2.4 promise_type的其他功能
promise_type还有一个额外的功能,即可对co_await的操作数进行转换处理。
struct promise_type {
…
awaitable await_transform(expr e) {
return awaitable(e);
}
};
除此之外,promise_type还可以对异常进行处理,为此我们需要给promise_type添加一个成员函数void set_exception
struct promise_type {
…
void unhandled_exception() {
eptr_ = std::current_exception();
}
};