当前位置 : 主页 > 编程语言 > c语言 >

C++ 20 协程总结

来源:互联网 收集:自由互联 发布时间:2023-09-07
C++ 20 协程总结 介绍 C++ 20提供的是非对称的、一等对象、无栈的协程(Coroutines in C++20 are asymmetric, first-class, and stackless) 所谓协程,即用户级线程,一种用于将异步代码同步化的编程机

C++ 20 协程总结

介绍

C++ 20提供的是非对称的、一等对象、无栈的协程(Coroutines in C++20 are asymmetric, first-class, and stackless)

所谓协程,即用户级线程,一种用于将异步代码同步化的编程机制,使得程序的执行流可以在多个并行事务之间切换但又不必承担切换带来的过高的性能损耗。当前很多的编程语言都内置协程特性或者有自己的协程库,如C/C++的libco、golang的goroutine等。而在实现机制上,又可以划分为有栈协程和无栈协程

协程是可以在保持状态的同时暂停和恢复执行的函数

C++ 20 协程总结_编译器

非对称协程与对称协程

非对称协程(asymmetric coroutines)是跟一个特定的调用者绑定的,协程让出CPU时,只能让回给原调用者。那到底是什么东西“不对称”呢?第一,非对称在于程序控制流转移到被调协程时使用的是suspend/resume操作,而当被调协程让出 CPU 时使用的却是return/yield操作。第二,协程间的地位也不对等,caller与callee关系是确定的,不可更改的,非对称协程只能返回最初调用它的协程。微信团队的libco其实就是一种非对称协程,Boost C++库也提供了非对称协程。另外,挂起(suspend)和恢复(resume)跟yield的区别是:yield后的协程,之后还会被切换回来,但是被suspend挂起的协程,除非调用resume()恢复它,否则永远不会再被执行到。在不同语言中,这三者会有不同的叫法,比如call也会调用新函数时也会同时实现suspend旧函数的功能,有的语言用yield/resume和return,不一而论,但区别不变。

对称协程(symmetric coroutines)则不同,被调协程启动之后就跟之前运行的协程没有任何关系了。协程的切换操作,一般而言只有一个操作yield或return,用于将程序控制流转移给另外的协程。对称协程机制一般需要一个调度器的支持,按一定调度算法去选择yield或return的目标协程。Go语言提供的协程,其实就是典型的对称协程。不但对称,goroutines还可以在多个线程上迁移。这种协程跟操作系统中的线程非常相似,甚至可以叫做“用户级线程”。

一等对象(第一类对象)

[python - What are "first-class" objects? - Stack Overflow](https://stackoverflow.com/questions/245192/what-are-first-class-objects

第一类对象(英语:First-class object)在计算机科学中指可以在执行期创造并作为参数传递给其他函数或存入一个变量的实体[1]。将一个实体变为第一类对象的过程叫做“物件化”(Reification)[2]。

无栈协程

有栈(stackful)协程通常的实现手段是在堆上提前分配一块较大的内存空间(比如 64K),也就是协程所谓的“栈”,参数、return address 等都可以存放在这个“栈”空间上。如果需要协程切换,那么通过 swapcontext 一类的形式来让系统认为这个堆上空间就是普通的栈,这就实现了上下文的切换。

有栈协程最大的优势就是侵入性小,使用起来非常简便,已有的业务代码几乎不需要做什么修改,但是 C++20 最终还是选择了使用无栈协程,主要出于下面这几个方面的考虑。

无栈协程是一个可以暂停和恢复的函数,是函数调用的泛化。

我们知道一个函数的函数体(function body)是顺序执行的,执行完之后将结果返回给调用者,我们没办法挂起它并稍后恢复它,只能等待它结束。而无栈协程则允许我们把函数挂起,然后在任意需要的时刻去恢复并执行函数体,相比普通函数,协程的函数体可以挂起并在任意时刻恢复执行。

C++ 20 协程总结_非对称_02

所以,从这个角度来说,无栈协程是普通函数的泛化。

总结一下,有栈协程是用户态线程,无栈协程就是函数调用

设计目标

  • 高度可伸缩性
  • 高效的恢复和挂起函数操作
  • 与已有设施无缝衔接,没有开销
  • 允许开发者设计协程库,开放高级语义的接口
  • 在禁用异常的环境可以使用

成为协程

一个函数成为一个协程,通过使用以下关键字中的一个

  • co_return
  • co_await
  • co_yield
  • 循环中的co_await

区分协程工厂和协程对象

术语协程通常用于协程的两个方面:一个是调用了co_awaitco_yieldco_return的函数,另一个是协程对象

使用一个协程术语形容协程的两个方面会让人糊涂

MyFuture<int> createFuture() {
co_return 2021; }
int main() {
auto fut = createFuture();
std::cout << "fut.get(): " << fut.get() << '\n'; }

函数createFuture是一个协程工厂返回一个协程对象。协程对象时一个可恢复对象,使用协程框架来指定他的行为

协程框架

实现协程的框架包含了20多个函数,一些必须实现,一些必须重写,因此你可以定制协程的功能

一个协程与三个部分相关:

  • promise object
  • coroutine handle
  • coroutine frame

通过coroutine handle协程句柄与promise object进行交互,并将上下文保存在coroutine frame

C++ 20 协程总结_句柄_03

编译器在协程执行过程中会自动调用这些函数

协程句柄(coroutine handle

协程句柄是一个非拥有的句柄,用于从外部恢复或销毁协程帧(frame)。协程句柄是可恢复函数的一部分。

template <typename T>
struct Generator
{
	struct promise_type;
	using handle_type = std::coroutine_handle<promise_type>;

	Generator(handle_type h): coro(h)
	{
	}

	handle_type coro;

	~Generator()
	{
		if (coro) coro.destroy();
	}

	T getValue()
	{
		return coro.promise().current_value;
	}

	bool next()
	{
		coro.resume();
		return not coro.done();
	}
}
  • 恢复协程执行:coro.resume()
  • 销毁协程:coro.destroy()
  • 检查状态:coro(15行)

协程帧Coroutine Frame

协程帧维持着协程堆内存的分配状态,包含promise_type,协程复制的参数,挂起点的表示,局部变量等

  • 协程的生命周期必须嵌套在调用者的生命周期内
  • 协程的调用者知到协程帧的大小

协程帧的关键是可等待体(**Awaitables **),等待器(Awaiters)

可等待体和等待器

promise_type中的三个函数返回可等待体 yield_value, initial_suspend, final_suspend

可等待体

可等待体决定协程是否暂停

本质上,编译器使用promise和co_await操作符生成这三个函数调用。

C++ 20 协程总结_非对称_04

co_await需要一个可等待体作为参数

实现可等待体需要三个函数

C++ 20 协程总结_编译器_05

C++20标准已经定义了两个基本的对象:std::suspend_alwaysstd::suspend_never

The Awaitable std::suspend_always

struct suspend_always {
constexpr bool await_ready() const noexcept { return false; }
constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};

总是挂起,await_ready返回false

The Awaitable std::suspend_never

struct suspend_never {
constexpr bool await_ready() const noexcept { return true; }
constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};

从不挂起,await_ready返回true

当协程协程执行的时候,这两个函数会自动执行:

  • 开始 initial_suspend
  • 结束 final_suspend

initial_suspend

当initial_suspend返回suspend_always时,协程会在开始时挂起;返回suspend_never时,则不会挂起

A lazy coroutine

std::suspend_always initial_suspend() {
return {};
}

A eager coroutine

std::suspend_never initial_suspend() {
return {};
}

final_suspend

在协程结束时执行,与几乎initial_suspend相同

可等待体和等待器

  • 可被等待的对象称为可等待(awaitable )体或者表达式;
  • co_await运算符必须返回一个等待器(awaiter):
  • 可等待体和等待器可以是同一个类型;
  • std::future(实验)是可等待体。
  • co_await运算符返回等待器_Future_awaiter

工作流

C++ 20 协程总结_非对称_06

编译器执行两个工作流外部的promise工作流和内部的awaiter工作流

Promise 工作流

当在函数中使用co_yield, co_await, co_return,函数成为一个协程,并且编译器将其转换成等价的如下代码

The transformed coroutine

C++ 20 协程总结_句柄_07

主要工作步骤:

  • 协程开始执行:
  • 申请必要的协程帧空间
  • 拷贝所有函数参数到协程帧
  • 创建promise_type对象
  • 调用promise_type中的get_return_object方法创建协程句柄(coroutine handle),并保持在局部变量中,当协程第一次挂起时,将返回给调用者
  • 调用initial_suspend并且co_await其结果,可能返回suspend_always/never
  • co_await prom.initial_suspend恢复resume时,函数体执行
  • 协程到达挂起点:
  • 返回对象(prom.get_return_object())将返回给恢复协程的调用程序
  • 协程到达co_return
  • 调用prom.retrun_void/value没有返回值或者返回值
  • 销毁变量
  • 调用prom.final_suspend()并且co_await它的结果
  • 协程销毁
  • 调用promise_type对象和函数参数对象的析构函数
  • 释放协程帧的内存
  • 返还控制权给调用者
  • 调用co_await执行等待器工作流

Awaiter工作流

调用co_await会让编译器执行三个函数:await_ready await_suspend await_resume

The generated Awaiter Workflow

C++ 20 协程总结_非对称_08

C++ 20 协程总结_编译器_09

只有await_ready返回false时,流程才会执行,否则的话直接返回await_resume的结果

await_ready返回false时:

  • 协程挂起,计算awaitable.await_suspend()的返回值,返回值有很多种情况

C++ 20 协程总结_非对称_10

出现异常情况不想写了

co_return

协程使用co_return作为返回语句

template <typename T>
struct MyFuture
{
	std::shared_ptr<T> value;

	MyFuture(std::shared_ptr<T> p): value(p)
	{
	}

	~MyFuture()
	{
	}

	T get()
	{
		return *value;
	}

	struct promise_type
	{
		std::shared_ptr<T> ptr = std::make_shared<T>();

		~promise_type()
		{
		}

		MyFuture<T> get_return_object() { return ptr; }

		void return_value(T v) { *ptr = v; }

		std::suspend_never initial_suspend() { return {}; }

		std::suspend_never final_suspend() noexcept { return {}; }

		void unhandled_exception()
		{
			std::terminate();
		}
	};
};

MyFuture<int> createFuture()
{
	co_return 2021;
}

int main(int argc, char* argv[])
{
	auto fut = createFuture();

	std::cout << fut.get() << std::endl;
}

C++ 20 协程总结_句柄_11

  • 流程
  • 初始化协程
  • 申请必要的协程帧空间
  • 拷贝所有函数参数到协程帧
  • 创建promise_type对象
  • 调用promise_type中的get_return_object方法将ptr传给fut
  • 调用co_return
  • 调用return_value并传入参数2022
  • 输出fut.get()

co_yield

无限数据流

template <typename T>
struct Generator
{
	struct promise_type;
	using handle_type = std::coroutine_handle<promise_type>;


	Generator(handle_type h): coro(h)
	{
	}

	handle_type coro;

	~Generator() { if (coro) coro.destroy(); }

	Generator(const Generator&) = delete;
	Generator& operator =(const Generator&) = delete;

	Generator(Generator&& oth) noexcept : coro(oth.coro)
	{
		oth.coro = nullptr;
	}

	Generator& operator =(Generator&& oth) noexcept
	{
		coro = oth.coro;
		oth.coro = nullptr;
		return *this;
	}

	T getValue()
	{
		return coro.promise().current_value;
	}

	bool next()
	{
		coro.resume();
		return !coro.done();
	}

	struct promise_type
	{
		promise_type() = default;
		~promise_type() = default;

		auto initial_suspend()
		{
			return std::suspend_always{};
		}

		auto final_suspend() noexcept
		{
			return std::suspend_always{};
		}

		auto get_return_object()
		{
			return Generator{handle_type::from_promise(*this)};
		}

		auto return_void()
		{
			return std::suspend_never{};
		}

		auto yield_value(const T value)
		{
			current_value = value;
			return std::suspend_always{};
		}

		void unhandled_exception()
		{
			std::terminate();
		}

		T current_value;
	};
};

Generator<int> getNext(int start = 0, int step = 1)
{
	auto value = start;
	while (true)
	{
		co_yield value;
		value += step;
	}
}

int main(int argc, char* argv[])
{
	auto gen = getNext();
	for (int i = 0; i <= 10; ++i)
	{
		gen.next();
		std::cout << std::format("gen value: {}\n", gen.getValue());
	}

	std::cout << "\n\n";

	auto gen2 = getNext(100, -10);
	for (int i = 0; i <= 20; ++i)
	{
		gen2.next();
		std::cout << std::format("gen2 value: {}\n", gen2.getValue());
	}
}

C++ 20 协程总结_非对称_12

看一下执行流程;

  • 创建promise_type
  • 调用get_return_object(),将其结果保存在局部变量
  • 创建generator
  • 调用initial_suspend挂起协程
  • 请求下一个值并消耗一个值然后挂起
  • 接着调用gen.next重复循环

co_await

struct Job
{
	struct promise_type;
	using handle_type = std::coroutine_handle<promise_type>;

	handle_type coro;

	Job(handle_type h): coro(h)
	{
	}

	~Job()
	{
		if (coro) coro.destroy();
	}

	void start()
	{
		coro.resume();
	}

	struct promise_type
	{
		auto get_return_object()
		{
			return Job{handle_type::from_promise(*this)};
		}

		std::suspend_always initial_suspend()
		{
			std::cout << "准备工作\n";
			return {};
		}

		std::suspend_always final_suspend() noexcept
		{
			std::cout << "执行工作\n";
			return {};
		}

		void return_void()
		{
		}

		void unhandled_exception()
		{
		}
	};
};

Job prepareJob()
{
	co_await std::suspend_never();
}


int main(int argc, char* argv[])
{
	std::cout << "工作之前\n";
	auto job = prepareJob();
	job.start();
	std::cout << "工作之后\n";
}
网友评论