0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

co_await这些协程时需要注意线程切换的细节

程序喵大人 来源:程序喵大人 作者:程序喵大人 2022-11-03 09:18 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

bfc58fd8-5b13-11ed-a3b6-dac502259ad0.png

在异步操作里,如异步连接、异步读写之类的协程,co_await这些协程时需要注意线程切换的细节。

以asio异步连接协程为例:

classclient{
public:
client(){
thd_=std::thread([this]{
io_ctx_.run();
});
}

async_simple::Lazyasync_connect(autohost,autoport){
boolret=co_awaitutil::async_connect(host,port);#1
co_returnret;#2
}

~client(){
io_ctx_.stop();
if(thd_.joinable()){
thd_.join();
}
}

private:
asio::io_contextio_ctx_;
std::threadthd_;
};

intmain(){
clientc;
async_simple::syncAwait(c.async_connect());
std::cout<<"quit
";#3
}

这个例子很简单,client在连接之后就析构了,看起来没什么问题。但是运行之后就会发生线程join的错误,错误的意思是在线程里join自己了。这是怎么回事?co_await一个异步连接的协程,当连接成功后协程返回,这时候发生了线程切换。异步连接返回的时候是在io_context的线程里,代码中的#1在主线程,#2在io_context线程,之后就co_return 返回到main函数的#3,这时候#3仍然在io_context线程里,接着client就会析构了,这时候仍然在io_context线程里,析构的时候会调用thd_.join(); 然后就导致了在io_context的线程里join自己的错误。

这是使用协程时容易犯错的一个地方,解决方法就是避免co_await回来之后去析构client,或者co_await回来仍然回到主线程。这里可以考虑用协程条件变量,在异步连接的时候发起一个新的协程并传入协程条件变量并在连接返回后set_value,主线程去co_await这个条件变量,这样连接返回后就回到主线程了,就可以解决在io线程里join自己的问题了。

bfc58fd8-5b13-11ed-a3b6-dac502259ad0.png

还是以上面的异步连接为例子,需要对之前的async_connect协程增加一个超时功能,代码稍作修改:

classclient{
public:
client():socket_(io_ctx_){
thd_=std::thread([this]{
io_ctx_.run();
});
}

async_simple::Lazyasync_connect(autohost,autoport,autoduration){
coro_timertimer(io_ctx_);
timeout(timer,duration).start([](auto&&){});//#1启动一个新协程做超时处理
boolret=co_awaitutil::async_connect(host,port,socket_);//假设这里co_await返回后回到主线程
co_returnret;
}

~client(){
io_ctx_.stop();
if(thd_.joinable()){
thd_.join();
}
}


private:
async_simple::Lazytimeout(auto&timer,autoduration){
boolis_timeout=co_awaittimer.async_wait(duration);
if(is_timeout){
asio::error_codeignored_ec;
socket_.shutdown(tcp::shutdown_both,ignored_ec);
socket_.close(ignored_ec);
}

co_return;
}

asio::io_contextio_ctx_;
tcp::socketsocket_;
std::threadthd_;
boolis_timeout_;
};

intmain(){
clientc;
async_simple::syncAwait(c.async_connect("localhost","9000",5s));
std::cout<<"quit
";#3
}

这个代码增加连接超时处理的协程,注意#1那里为什么需要新启动一个协程,而不能用co_await呢?因为co_await是阻塞语义,co_await会导致永远超时,启动一个新的协程不会阻塞当前协程从而可以去调用async_connect。

当timeout超时发生时就关闭socket,这时候async_connect就会返回错误然后返回到调用者,这看起来似乎可以对异步连接做超时处理了,但是这个代码是有问题的。假如异步连接没有超时会发生什么?没有超时的话就返回到main函数了,然后client就析构了,当timeout协程resume回来的时候client其实已经析构了,这时候再去调用成员变量socket_ close将会导致一个访问已经析构对象的错误。

也许有人会说,那就在co_return之前去取消timer不就好了吗?这个办法也不行,因为取消timer,timeout协程并不会立即返回,仍然会存在访问已经析构对象的问题。

正确的做法应该是对两个协程进行同步,timeout协程和async_connect协程需要同步,在async_connect协程返回之前需要确保timeout协程已经完成,这样就可以避免访问已经析构对象的问题了。

这个问题其实也是异步回调安全返回的一个经典问题,协程也同样会遇到这个问题,上面提到的对两个协程进行同步是解决方法之一,另外一个方法就是使用shared_from_this,就像异步安全回调那样处理。

还是以异步连接为例:

async_simple::Lazyasync_connect(conststd::string&host,conststd::string&port){
co_returnco_awaitutil::async_connect(host,port);
}

async_simple::Lazytest_connect(){
boolok=co_awaitasync_connect("localhost","8000");
if(!ok){
std::cout<<"connectfailed
";
}

std::cout<<"connectok
";
}

intmain(){
async_simple::syncAwait(test_connect());
}

这个代码简单明了,就是测试一下异步连接是否成功,运行也是正常的。如果稍微改一下test_connect:

async_simple::Lazytest_connect(){
autolazy=async_connect("localhost","8000");
boolok=co_awaitlazy;
if(!ok){
std::cout<<"connectfailed
";
}

std::cout<<"connectok
";
}

很遗憾,这个代码会导致连接总是失败,似乎很奇怪,后面发现原因是因为async_connect的两个参数失效了,但是写法和刚开始的写法几乎一样,为啥后面这种写法会导致参数失效呢?

原因是co_await一个协程函数时,其实做了两件事:

  • 调用协程函数创建协程,这个步骤会创建协程帧,把参数和局部变量拷贝到协程帧里;

  • co_await执行协程函数;

回过头来看auto lazy = async_connect("localhost", "8000"); 这个代码调用协程函数创建了协程,这时候拷贝到协程帧里面的是两个临时变量,在这一行结束的时候临时变量就析构了,在下一行去co_await执行这个协程的时候就会出现参数失效的问题了。

co_await async_connect("localhost", "8000"); 这样为什么没问题呢,因为协程创建和协程调用都在一行完成的,临时变量知道协程执行之后才会失效,因此不会有问题。

问题的本质其实是C++临时变量生命周期的问题。使用协程的时候稍微注意一下就好了,可以把const std::string&改成std::string,这样就不会临时变量生命周期的问题了,如果不想改参数类型就co_await 协程函数就好了,不分成两行去执行协程。

审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 变量
    +关注

    关注

    0

    文章

    615

    浏览量

    29371
  • 线程
    +关注

    关注

    0

    文章

    508

    浏览量

    20761

原文标题:C++ 使用协程需要注意的问题

文章出处:【微信号:程序喵大人,微信公众号:程序喵大人】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    FreeRTOS任务和协的区别是什么

    1.堆栈 是没有堆栈分配的,是所有创建的共同使用一个堆栈空间,这相比于任务来说,减少了RAM的使用空间。 2. 调度和优先级
    发表于 12-08 08:18

    用户在使用GPIO反跳功能时需要注意哪些限制?

    用户在使用GPIO反跳功能时需要注意哪些限制?
    发表于 08-26 06:32

    请问用户在使用GPIO反跳功能时需要注意哪些限制?

    用户在使用GPIO反跳功能时需要注意哪些限制?
    发表于 08-22 07:03

    振弦式土体沉降计钻孔埋设需要注意什么?

    、确保长期监测稳定性的前提。振弦式土体沉降计钻孔埋设需要注意什么?关键注意事项规避施工风险工程实践中需重点管控四类风险:钻孔质量控制:倾斜度偏差需≤2°,防止仪器偏斜
    的头像 发表于 08-19 13:56 468次阅读
    振弦式土体沉降计钻孔埋设<b class='flag-5'>需要注意</b>什么?

    企业选择SDWAN方案时,需要注意哪些?

    ##企业选择SDWAN方案时,需要注意哪些?在数字化转型浪潮中,企业广域网正经历从“连通即可”向“智能、安全、云原生”的深刻变革。SD-WAN技术凭借其颠覆性的架构理念,成为企业优化网络性能
    的头像 发表于 08-15 10:03 1304次阅读
    企业选择SDWAN方案时,<b class='flag-5'>需要注意</b>哪些?

    IR615S桥接AP,在相同SSID 的AP间不能切换要注意哪些设置?

    现场有多个AP,用相同的SSID,没有使用AC,现场IR615S WiFi桥接AP WiFi时,当连接的AP关闭后,不能切换到到其他AP上,需要注意哪些设置?
    发表于 08-06 07:39

    请问工程移植都有哪些需要注意的地方?

    ST的固件库还是挺丰富的,有时候我们直接移植工程还是挺方便的,不过总是会有各种各样的报错存在,在移植的时候有哪些需要注意的吗?或者一些常见的报错如何解决?
    发表于 07-11 06:50

    使用STM32CubeMX进行配置USB的时候,有哪些小的需要注意细节

    在使用STM32CubeMX进行配置USB的时候,是参照例程进行的配置,自己配置的就是没有成功,例程就灭有问题,总感觉哪里没有打开?有人遇到过需要注意的小细节吗?
    发表于 04-23 06:54

    LuatOS深度解析:小白也能10分钟学会,代码效率直接起飞!

    嵌入式开发如何兼顾效率与简洁?LuatOS给出完美答案!它用类线程的语法封装异步逻辑,让多任务开发像单线程一样简单。本文用图文并茂的方式拆解
    的头像 发表于 04-10 15:23 455次阅读
    LuatOS<b class='flag-5'>协</b><b class='flag-5'>程</b>深度解析:小白也能10分钟学会,代码效率直接起飞!

    10分钟上手写代码,LuatOS轻松掌握!

    10分钟学会LuatOS,从此你的程序也能像通勤族利用碎片时间一样游刃有余。现在就去动手试一试,开启异步编程新体验! 写给第一次听说的你‌: 别怕!
    的头像 发表于 04-10 15:18 494次阅读
    10分钟上手写代码,LuatOS<b class='flag-5'>协</b><b class='flag-5'>程</b>轻松掌握!

    稳压器在安装接线前需要注意哪些

    稳压器是一种非常重要的电气设备,它可以有效地解决电压不稳定、波动过大等问题,保证设备的正常运行,然而,稳压器接线并非简单地将线接好就行,而是需要注意一些事项,以确保其能够安全、有效地发挥应有的作用,下面小编来说说稳压器在安装接线前需要注意哪些。
    的头像 发表于 04-03 15:20 660次阅读
    稳压器在安装接线前<b class='flag-5'>需要注意</b>哪些

    进程、线程傻傻分不清?一文带你彻底扒光它们的\"底裤\"!

    权(yield)实现协作,单线程内玩出多任务的感觉。 技术细节切换成本≈打哈欠(0.1μs~1μs) 阻塞操作会直接让出CPU(比如
    发表于 03-26 09:27

    变频器控制回路布线需要注意什么

    变频器控制回路布线时需要注意的几个方面,以确保布线的专业性和稳定性。 一、避免干扰:主电路与控制回路分离 变频器的主电路(动力线)通常包含高电压、大电流,因此会产生较强的电磁场,这些电磁场可能干扰控制回路的
    的头像 发表于 03-11 15:33 898次阅读
    变频器控制回路布线<b class='flag-5'>需要注意</b>什么

    速度探头在使用过程中需要注意哪些问题呢

    速度探头在使用过程中需要注意安装与维护、参数设置与校准、使用注意事项以及安全注意事项等多个方面。只有做好这些工作,才能确保探头的正常工作、测量精度和安全性。
    的头像 发表于 02-06 15:11 766次阅读

    ADS1247想配置为单端输入,都需要注意哪些寄存器?

    这些开关是由哪些寄存器控制的,我在手册里怎么没有找到,我想配置为单端输入,不知道都需要注意哪些寄存器,在线等答案,望各位大侠帮忙
    发表于 01-15 08:31