C++协程 c++协程原理
itomcoil 2024-12-24 13:28 14 浏览
协程概念
协程是一个可以暂停执行以便稍后恢复的函数。
协程是无堆栈的:通过返回给调用者来暂停执行,并且恢复执行所需的数据与堆栈分开存储。
这允许异步执行顺序的代码(例如,在没有显式回调的情况下处理非阻塞 I/O),并且还支持惰性计算无限序列和其他用途的算法。
如果函数的定义执行以下任何一项,则该函数是协程:
1、使用 co_await 运算符暂停执行直到恢复为止
task<> tcp_echo_server()
{
char data[1024];
while(true)
{
size_t n = co_await socket.async_read_some(buffer[data]);
co_await async_write(socket,buffer(data,n));
}
}
2、使用关键字 co_yield 暂停执行返回值
generator<int> itoa(int n = 0)
{
while(true)
{
co_yield n++;
}
}
3、使用关键字 co_return 完成执行返回值
lazy<int> f()
{
co_return 7;
}
每个协程都必须具有满足许多要求的返回类型,如下所述:
限制
协程不能使用可变参数、普通返回语句或占位符返回类型(auto 或 Concept)。
constexpr 函数、构造函数、析构函数和主函数不能是协程。
执行
每个协程都与
a、从协程内部操纵的承诺对象。 协程通过此对象提交其结果或异常。
b、协程句柄,从协程外部操作。 这是一个非拥有句柄,用于恢复协程的执行或销毁协程框架。
c、协程状态,它是一个内部的堆分配(除非分配被优化),对象包含
1)承诺(promise)对象
2)参数(全部按值复制)
3)当前暂停点的某种表示,以便 resume 知道在哪里继续, destroy 知道哪些局部变量在范围内
4)生命周期跨越当前挂起点的局部变量和临时变量
当协程开始执行时,它会执行以下操作:
1、使用 operator new 分配协程状态对象(见下文)
2、将所有函数参数复制到协程状态:移动或复制按值复制参数,按引用参数保持引用(如果在引用对象的生命周期结束后恢复协程,则可能会变得悬空)
#include <coroutine>
#include <iostream>
struct promise;
struct coroutine : std::coroutine_handle<promise>
{ using promise_type = struct promise; };
struct promise {
coroutine get_return_object()
{ return {coroutine::from_promise(*this)}; }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
struct S {
int i;
coroutine f() {
std::cout << i;
co_return;
}
};
void bad1() {
coroutine h = S{0}.f();
// S{0} destroyed
h.resume(); // resumed coroutine executes std::cout << i, uses S::i after free
h.destroy();
}
coroutine bad2() {
S s{0};
return s.f(); // returned coroutine can't be resumed without committing use after free
}
void bad3() {
coroutine h = [i = 0]() -> coroutine { // a lambda that's also a coroutine
std::cout << i;
co_return;
}(); // immediately invoked
// lambda destroyed
h.resume(); // uses (anonymous lambda type)::i after free
h.destroy();
}
void good() {
coroutine h = [](int i) -> coroutine { // make i a coroutine parameter
std::cout << i;
co_return;
}(0);
// lambda destroyed
h.resume(); // no problem, i has been copied to the coroutine frame as a by-value parameter
h.destroy();
}
3、调用 promise 对象的构造函数。 如果 Promise 类型有一个接受所有协程参数的构造函数,则调用该构造函数,并带有复制后的协程参数。 否则调用默认构造函数。
4、调用 promise.get_return_object() 并将结果保存在局部变量中。 当协程第一次挂起时,该调用的结果将返回给调用者。 任何在此步骤之前(包括该步骤)引发的异常都会传给调用者,而不是放在 Promise 中。
5、调用 promise.initial_suspend() 并 co_await 其结果。 典型的 Promise 类型要么返回一个 suspend_always,用于延迟启动的协程,要么返回一个 suspend_never,用于急切启动的协程。
6、当 co_await promise.initial_suspend() 恢复时,开始执行协程主体。
7、当协程到达暂停点时,如果需要,在隐式转换为协程的返回类型后,将之前获得的返回对象返回给调用者/恢复者。
8、当协程到达 co_return 语句时,它会执行以下操作:
1)调用 promise.return_void() 为
a、co_return;
b、co_return expr 其中 expr 的类型为 void
c、从返回 void 的协程的末尾掉落。 在这种情况下,如果 Promise 类型没有 Promise::return_void() 成员函数,则行为未定义。
2)或为 co_return expr 调用 promise.return_value(expr),其中 expr 具有非 void 类型
3)以与创建时相反的顺序销毁所有具有自动存储持续时间的变量。
4)调用 promise.final_suspend() 并 co_await 其结果。
9、如果协程以未捕获的异常结束,它将执行以下操作:
1)捕获异常并从 catch-block 中调用 promise.unhandled_exception()
2)调用 promise.final_suspend() 并 co_await 结果(例如,恢复协程或获取结果)。 从这一点恢复协程是未定义的行为。
10、当协程状态因为 co_return 或未捕获的异常终止,或者因为它的句柄被销毁而被销毁时,它会执行以下操作:
1)调用 promise 对象的析构函数。
2)调用函数参数副本的析构函数。
3)调用 operator delete 释放协程状态使用的内存
4)将执行转移给调用者/resumer。
堆分配
协程状态通过非数组 operator new 在堆上分配。
如果 Promise 类型定义了类级别的替换,则将使用它,否则将使用全局的 operator new。
如果 Promise 类型定义了一个带有附加参数的 operator new 的放置形式,并且它们匹配一个参数列表,其中第一个参数是请求的大小(类型为 std::size_t),其余的是协程函数参数,这些参数将 传递给 operator new(这使得协程可以使用前导分配器约定)。
可以优化对 operator new 的调用(即使使用自定义分配器),如果协程状态的生命周期严格嵌套在调用者的生命周期内,并且协程框架的大小在调用站点是已知的。在这种情况下,协程状态嵌入在调用者的堆栈帧中(如果调用者是普通函数)或协程状态(如果调用者是协程)
如果分配失败,协程抛出 std::bad_alloc,除非 Promise 类型定义了成员函数 Promise::get_return_object_on_allocation_failure()。 如果定义了该成员函数,则分配使用 operator new 的 nothrow 形式,并且在分配失败时,协程立即将从 Promise::get_return_object_on_allocation_failure() 获得的对象返回给调用者。
Promise
Promise 类型由编译器使用 std::coroutine_traits 根据协程的返回类型确定。
形式上,令 R 和 Args... 分别表示协程的返回类型和参数类型列表,Class T 和 /*cv-qual*/(如果有)分别表示协程所属的类类型及其 cv-qualification。如果它被定义为非静态成员函数,它的 Promise 类型由以下决定:
1、std::coroutine_traits<R, Args...>::promise_type,如果协程没有定义为非静态成员函数,
2、std::coroutine_traits<R, Class T /*cv-qual*/&, Args...>::promise_type,如果协程被定义为非右值引用限定的非静态成员函数,
3、std::coroutine_traits<R, Class T /*cv-qual*/&&, Args...>::promise_type,如果协程被定义为右值引用限定的非静态成员函数。
例如:
1、如果协程定义为 task<float> foo(std::string x, bool flag);,则其 Promise 类型为 std::coroutine_traits<task<float>, std::string, bool>::promise_type。
2、如果协程定义为 task<void> my_class::method1(int x) const;,则其 Promise 类型为 std::coroutine_traits<task<void>, const my_class&, int>::promise_type。
3、如果协程定义为 task<void> my_class::method1(int x) &&;,则其 Promise 类型为 std::coroutine_traits<task<void>, my_class&&, int>::promise_type。
co_await
unary operator co_await 挂起协程并将控制权返回给调用者。 它的操作数是一个表达式,其类型必须要么定义 operator co_await,要么可以通过当前协程的 Promise::await_transform 转换为这种类型。
co_await expr
首先,将 expr 转换为可等待对象,如下所示:
1、如果 expr 由初始挂起点、最终挂起点或 yield 表达式产生,则awaitable是 expr。
2、否则,如果当前协程的 Promise 类型有成员函数 await_transform,那么 awaitable 就是 promise.await_transform(expr)。
3、否则,awaitable 就是 expr
然后,得到awaiter对象,如下:
1、如果 operator co_await 的重载决议给出了单个最佳重载,则awaiter是该调用的结果(对于成员重载,awaitable.operator co_await(),对于非成员重载,operator co_await(static_cast<Awaitable&&>(awaitable)))
2、否则,如果重载决议没有找到 operator co_await,则awaiter是可等待的。
3、否则,如果重载决议不明确,则程序格式错误。
如果上面的表达式是纯右值,则 awaiter 对象是它的临时物化对象。 否则,如果上面的表达式是一个泛左值,那么 awaiter 对象就是它所引用的对象。
然后,调用 awaiter.await_ready() (如果知道结果已准备好或可以同步完成,这是避免挂起成本的捷径)。 如果其结果,上下文转换为 bool 为假,则协程被挂起(其协程状态由局部变量和当前挂起点填充)。
awaiter.await_suspend(handle) 被调用,其中 handle 是表示当前协程的协程句柄。 在该函数内部,挂起的协程状态可以通过该句柄观察到,并且该函数有责任安排它在某个执行程序上恢复或被销毁(返回错误计数作为调度)
1、如果 await_suspend 返回 void,则立即将控制权返回给当前协程的调用者/恢复者(此协程保持挂起状态)。
2、如果 await_suspend 返回布尔值,值 true 将控制权返回给当前协程的调用者/恢复者;值 false 恢复当前协程。
3、如果 await_suspend 返回某个其他协程的协程句柄,则恢复该句柄(通过调用 handle.resume())(注意这可能会链接到最终导致当前协程恢复)
4、如果 await_suspend 抛出异常,则捕获异常,恢复协程,立即重新抛出异常
最后,调用 awaiter.await_resume() (无论协程是否挂起),其结果是整个 co_await expr 表达式的结果。
如果协程在 co_await 表达式中被挂起,然后又被恢复,那么恢复点就在调用 awaiter.await_resume() 之前。
注意,由于协程在进入 awaiter.await_suspend() 之前已完全挂起,因此该函数可以自由地跨线程传输协程句柄,而无需额外同步。例如,它可以将其放入回调中,计划在异步I/O操作完成时在线程池上运行。在这种情况下,由于当前协同路由可能已经恢复,因此执行了waiter对象的析构函数,当await_suspend()继续在当前线程上执行时,await_suspend()应将*this视为已销毁,并且在句柄发布到其他线程后不访问它。
Example
#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>
auto switch_to_new_thread(std::jthread& out) {
struct awaitable {
std::jthread* p_out;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
std::jthread& out = *p_out;
if (out.joinable())
throw std::runtime_error("Output jthread parameter not empty");
out = std::jthread([h] { h.resume(); });
// Potential undefined behavior: accessing potentially destroyed *this
// std::cout << "New thread ID: " << p_out->get_id() << '\n';
std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK
}
void await_resume() {}
};
return awaitable{&out};
}
struct task{
struct promise_type {
task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
task resuming_on_new_thread(std::jthread& out) {
std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
co_await switch_to_new_thread(out);
// awaiter destroyed here
std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
}
int main() {
std::jthread out;
resuming_on_new_thread(out);
}
Possible output:
Coroutine started on thread: 140193066129216
New thread ID: 140193045907200
Coroutine resumed on thread: 140193045907200
注意:awaiter 对象是协程状态的一部分(作为一个临时对象,其生命周期跨越一个暂停点),并在 co_await 表达式完成之前被销毁。 它可用于根据某些异步 I/O API 的要求维护每个操作的状态,而无需求助于额外的堆分配。
标准库定义了两个简单的等待项:std::suspend_always 和 std::suspend_never。
co_yield
Yield-expression 向调用者返回一个值并暂停当前协程:它是可恢复生成器函数的通用构建块
co_yield expr
co_yield braced-init-list
它相当于
co_await promise.yield_value(expr)
典型的生成器的 yield_value 会将其参数存储(复制/移动或仅存储其地址,因为参数的生命周期跨越 co_await 内的暂停点)到生成器对象中并返回 std::suspend_always,将控制权转移给调用者/恢复者。
#include <coroutine>
#include <exception>
#include <iostream>
template<typename T>
struct Generator {
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
struct promise_type { // required
T value_;
std::exception_ptr exception_;
Generator get_return_object() {
return Generator(handle_type::from_promise(*this));
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { exception_ = std::current_exception(); } // saving
// exception
template<std::convertible_to<T> From> // C++20 concept
std::suspend_always yield_value(From &&from) {
value_ = std::forward<From>(from); // caching the result in promise
return {};
}
void return_void() {}
};
handle_type h_;
Generator(handle_type h) : h_(h) {}
~Generator() { h_.destroy(); }
explicit operator bool() {
fill();
/*
唯一可靠地确定我们是否完成协程,是否将通过 C++ getter(下面的operator())在协程中生成下
一个值(co_yield)的唯一方法是执行/恢复协程,直到下一个 co_yield 点(或让它脱落)。然后
我们存储/缓存结果以承诺允许getter(下面的operator()在不执行协程的情况下获取它)。
*/
return !h_.done();
}
T operator()() {
fill();
full_ = false; // 我们将移出先前缓存的结果以使 promise 再次为空
return std::move(h_.promise().value_);
}
private:
bool full_ = false;
void fill() {
if (!full_) {
h_();
if (h_.promise().exception_)
std::rethrow_exception(h_.promise().exception_);
// 在被调用的上下文中传播协程异常
full_ = true;
}
}
};
Generator<uint64_t>
fibonacci_sequence(unsigned n)
{
if (n==0)
co_return;
if (n>94)
throw std::runtime_error("Too big Fibonacci sequence. Elements would overflow.");
co_yield 0;
if (n==1)
co_return;
co_yield 1;
if (n==2)
co_return;
uint64_t a=0;
uint64_t b=1;
for (unsigned i = 2; i < n; i++)
{
uint64_t s=a+b;
co_yield s;
a=b;
b=s;
}
}
int main()
{
try {
auto gen = fibonacci_sequence(10); // max 94 before uint64_t overflows
for (int j=0; gen; j++)
std::cout << "fib("<<j <<")=" << gen() << '\n';
}
catch (const std::exception& ex)
{
std::cerr << "Exception: " << ex.what() << '\n';
}
catch (...)
{
std::cerr << "Unknown exception.\n";
}
}
Output:
fib(0)=0
fib(1)=1
fib(2)=1
fib(3)=2
fib(4)=3
fib(5)=5
fib(6)=8
fib(7)=13
fib(8)=21
fib(9)=34
相关推荐
- PS小技巧 调整命令,让人物肤色变得更加白皙 #后期修图
-
我们来看一下如何去将人物的皮肤变得更加的白皙。·首先选中图层,Ctrl键加J键复制一层。·打开这里的属性面板,选择快速操作删除背景,这样就会将人物进行单独的抠取。·接下来在上方去添加一个黑白调整图层,...
- 把人物肤色提亮的方法和技巧
-
PS后期调白肤色提亮照片的方法。一白遮百丑,所以对于Photoshop后期来说把人物肤色调白是一项非常重要的任务。就拿这张素材图片来说,这张素材图片人脸的肤色主要偏红、偏黄,也不够白皙,该怎样对它进行...
- 《Photoshop教程》把美女图片调成清爽色彩及润肤技巧
-
关注PS精品教程,每天不断更新~~室内人物图片一般会偏暗,人物脸部、肤色及背景会出现一些杂点。处理之前需要认真的给人物磨皮及美白,然后再整体润色。最终效果原图一、用修补工具及图章工具简单去除大一点的黑...
- PS后期对皮肤进行美白的技巧
-
PS后期进行皮肤美白的技巧。PS后期对皮肤进行美白的技巧:·打开素材图片之后直接复制原图。·接下来直接点击上方的图像,选择应用图像命令。·在通道这里直接选择红通道,混合这里直接选择柔光,然后点击确定。...
- 493 [PS调色]调模特通透肤色
-
效果对比:效果图吧:1、光位图:2、拍摄参数:·快门:160;光圈:8;ISO:1003、步骤分解图:用曲线调整图层调出基本色调。用可选颜色调整图层调整红色、黄色、白色和灰色4种颜色的混合比例。用色彩...
- 先选肤色再涂面部,卡戴珊的摄影师透露:为明星拍完照后怎么修图
-
据英国媒体12月17日报道,真人秀明星金·卡戴珊终于承认,她把女儿小北P进了家族的圣诞贺卡,怪不得粉丝们都表示这张贺卡照得非常失败。上周,这位39岁的女星遭到了一些粉丝针对这张照片的批评,她于当地时间...
- 如何在PS中运用曲线复制另一张照片的色调
-
怎样把另一张作品的外观感觉,套用到自己的照片上?单靠肉眼来猜,可能很不容易,而来自BenSecret的教学,关键是在PS使用了两个工具,让你可以准确比较两张照片的曝光、色调与饱和度,方便你调整及复制...
- PS在LAB模式下调出水嫩肤色的美女
-
本PS教程主要使用Photoshop使用LAB模式调出水嫩肤色的美女,教程调色比较独特。作者比较注重图片高光部分的颜色,增加质感及肤色调红润等都是在高光区域完成。尤其在Lab模式下,用高光选区调色后图...
- 在Photoshop图像后期处理中如何将人物皮肤处理得白皙通透
-
我们在人像后期处理中,需要将人物皮肤处理的白皙通透,处理方法很多,大多数都喜欢使用曲线、磨皮等进行调整,可以达到亮但是不透,最终效果往往不是很好,今天就教大家一种如何将任务皮肤处理得白皙通透,希望能帮...
- PS调色自学教程:宝宝照片快速调通透,简单实用!
-
PS调色自学教程:宝宝照片快速调通透。·首先复制图层,然后选择进入ACR滤镜,选择曲线锁定照片的亮部,也就高光位置,其他部位补亮一点,尤其是阴影的部位补亮多一些,让画面的层次均匀一点。·然后回到基本项...
- 【干货】如何利用PS进行人物美化
-
人物图像美化在Photoshop中非常常用,Photoshop作为一款功能强大的图像处理软件,不仅可以对人像进行基本的调色、美化和修复等处理,还可以改变人物的线条和幅度,如调整脸部器官和脸型的大小、调...
- 教大家一种可以快速把肤色处理均匀的方法@抖音短视频
-
快速把肤色处理均匀的方法。今天教大家一种可以快速把肤色处理均匀的方法。像这张照片整体肤色走紫红色,但是局部偏黄缘处理起来非常的麻烦。其实我们只需要新建空白图层,图层混合模式更改为颜色,再选择画笔工具把...
- PS调色教程 利用RAW调出干净通透的肤色
-
要么不发,要么干货。后期教程来噜~用RAW调出干净通透的肤色。这次终于不会原片比PS后好看了吧。如果你依然这么觉得,请不要残忍的告诉我这个事实,泪谢TAT)附送拍摄花絮,感谢各位的支持更多风格请关注m...
- photoshop后期皮肤变白的技巧
-
PS后期皮肤变白的技巧。1.PS后期让皮肤变白的方法有很多种,接下来教你一种非常简单容易上手的方法。2.打开素材图片之后,直接在小太极下拉框的位置添加一个纯色调整图层,颜色设置一个纯白色,点击...
- Photoshop调出人物的淡雅粉嫩肤色教程
-
本教程主要使用Photoshop调出人物的淡雅粉嫩肤色教程,最终的效果非常的通透迷人,下面让我们一起来学习.出自:86ps效果图:原图:1、打开原图复制一层。2、用Topaz滤镜磨皮(点此下载)。3、...
- 一周热门
- 最近发表
- 标签列表
-
- ps像素和厘米换算 (32)
- ps图案在哪里 (33)
- super().__init__ (33)
- python 获取日期 (34)
- 0xa (36)
- super().__init__()详解 (33)
- python安装包在哪里找 (33)
- linux查看python版本信息 (35)
- python怎么改成中文 (35)
- php文件怎么在浏览器运行 (33)
- eval在python中的意思 (33)
- python安装opencv库 (35)
- python div (34)
- sticky css (33)
- python中random.randint()函数 (34)
- python去掉字符串中的指定字符 (33)
- python入门经典100题 (34)
- anaconda安装路径 (34)
- yield和return的区别 (33)
- 1到10的阶乘之和是多少 (35)
- python安装sklearn库 (33)
- dom和bom区别 (33)
- js 替换指定位置的字符 (33)
- python判断元素是否存在 (33)
- sorted key (33)