现代C++语言核心特性解析part17

  • Post author:
  • Post category:其他




第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();
	}
};



版权声明:本文为qq_46365592原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。