rust嵌入式开发之await

嵌入式经常有类似通过串口发送指令然后等待响应再做出进一步反应的需求。比如,通过串口以AT命令来操作蓝牙模块执行扫描、连接,需要根据实际情况进行操作,复杂的可能需要执行7、8条指令才能完成连接。

对于这样的需求,如果用异步编程的方式来做,由于收发双方被解除了耦合,所以需要设置多种状态标记来对齐收发双方的步调,逻辑上非常不直观、操作上也非常繁琐,稍有疏漏就会出现bug,而且也不便于扩展【新功能需要重新修订状态空间,这就会对现有功能产生潜在的干扰】。所以我们一般都是将其串行化来简化编程。

在用c基于rt-thread开发时,这样的异步操作串行化我是通过系统提供的线程间通信工具【信号量】+用宏进行简写来实现的。

现在,当我们基于Embassy来开发嵌入式时,rust提供的async/await就是一套标准化的将并发操作进行串行化的工具。

理解await

rust中async/await的核心就是Future【一个需要系统介入的trait】:

1、async就是告诉编译器:我不是普通函数,我是一个Future,需要特殊对待

2、await则通知编译器,对于一个Future,需要按如下方式来操作:

  • 为调用我,需要生成一个任务
  • 将这个任务打包成一个可wake的上下文【当然还包括其它准备工作】
  • 设置一个等待点,当任务执行完毕时会返回到该处并带回结果,然后从等待点继续执行
  • 控制流转向这个任务,即启动这个任务,然后在其中执行我的poll函数【以该上下文为参数】

这个任务属于系统任务,由rust的运行时负责管理与调度。

我们完整描述一下await的异步操作串行化过程:

  • 当前执行流启动异步操作后,发出await指令
  • await指令会导致控制权被转移给rust的系统运行时
  • 系统运行时启动一个用于检查该异步操作是否执行完毕的poll任务,这个poll任务将由系统按需调度
  • poll任务每次被调度执行时都会检查异步操作的执行情况
  • 检查后,如果通知异步操作还没执行完,系统就挂起poll任务并等待下一次调度机会
  • 检查后,如果通知异步操作执行完毕了,系统就结束poll任务并带着异步操作的执行结果返回到await指令发出点
  • 原来的执行流得到异步操作的执行结果,然后继续执行

大家可以看到,异步操作串行化需要完成大量的工作,在没有await的情况下,如我基于rtt的实现中,就全部需要我们自己来实现了。当然,我们自己的实现不需要考虑通用性,自然也就不会这么复杂,但主要的逻辑都是需要覆盖到的。

由我们自己编写的、负责检查异步任务是否完成的poll函数就两个返回值:

  • Pending:指示异步操作还没有完成,则将poll任务丢进等待队列沉睡,等待wake后重新检查【即再次执行poll函数】
  • Ready(data):指示异步操作执行完毕,则将poll任务从等待队列中踢出去,然后将Ready所包裹的数据作为结果返回调用任务时的等待点继续执行

现在就只有一个问题了:poll任务什么时候会被投入运行呢?!准备好的就可以执行。

那么,怎么才叫准备好的呢?!没执行过的、从沉睡中被wake的。

那么,怎么wake呢?!三种方式:

  • 我们手动提取到waker,交给对应的系统部件,如放入时钟任务队列,由系统wake
  • 我们手动提取到waker,自己保存起来,等条件满足时,我们自己wake
  • 系统提取到waker,自动帮我们wake

就上述过程,Embassy/rust中存在两种特例:

1、Timer超时

如果我们需要原地等待上一段时间,我们一般会:

//等待10毫秒
Timer::after_millis(10).await;

这个时候并没有需要等待的异步操作,就单纯的等待时间的流逝。所以,就是把waker推入系统时钟队列,等待唤醒即可。

2、async标记的函数【rust中就是一个Future】

这个函数就是需要等待的异步操作。

这个时候,出现了三个执行流:

  • 原执行流,由于await指令的调用,该执行流暂停
  • 系统执行流,由于await指令的调用,系统运行时获得了执行权,其需要完成一系列的相关工作
  • 异步执行流,系统运行时会创建一个新的任务,并在其中执行被async标记的异步函数,该函数什么时候会得到执行,理论上由系统运行时按需进行调度

当然,我们说的是理论上,不同的运行时自然有自己的考虑。

考虑到大家的习惯,我们一般都把异步操作封装到异步函数中来执行。但大家一定要理解:await是一个系统指令,和是否调用了async标记的函数没有必然的联系。

我们完全可以先创建一个Future,然后执行一系列准备工作并启动一个异步操作,然后await这个Future。而这,也正是await的标准使用方法,只不过我们经常会将执行准备工作和启动异步操作都放到一个函数中来做,最后由这个函数返回一个Future。所以大家才形成了async/await配对使用的印象。

但这个印象当然是错误的:只要是Future就可以await,async只不过简化了【创建一个匿名struct、为其编写poll函数及Future、将异步函数封装进一个隐式函数中完成创建该匿名struct、调用函数、返回刚创建的匿名struct】这一繁琐的过程而已。

作为运行时的Embassy

Embassy实现了一个任务执行器,rust编译器会自动将其装配成系统运行时,我们写好自己的Future,然后在需要时await即可,rust编译器会配合Embassy的任务执行器帮助我们自动完成上面罗列的一大堆工作。

Embassy还实现了时钟任务队列,我们可以借用来实现超时。但需要我们显式导入,即首先需要在Cargo.toml中声明对应的依赖:

[dependencies]
...其它依赖
embassy-time-queue-driver = "0.1.0"

然后在我们的程序中使用:

let waker = cx.waker();
embassy_time_queue_driver::schedule_wake(self.expires_at.as_ticks(), waker);
举个例子

我们的目标是实现:串口发送命令后等待如下响应

  • 接收正确
  • 接收错误
  • 接收超时【未得到任何响应】

注:何为正确、何为错误,这属于业务上的判断,对我们这个例子来说,不需要关心

我们需要为这个响应设计一个数据结构:

#[derive(Clone)]
pub struct ExpectResult {//超时检测的最后期限expires_at: Instant,//响应处理完毕的标记ready: bool,//负责叫醒任务的服务员waker: Option<core::task::Waker>,//区分响应状态的一个结果码枚举rc: ExpectResultCode,//执行结果,根据我们的业务来设计,各种形式都可以result: (...我们需要的数据...),
}

注:考虑rust的借用,一般需要抽离成一个内部的struct来做Future,外部只做包裹以及必要的操作接口。但为便于说明,这一部分就略去了

一般来说,作为基础结构,我们都会用泛型实现通用代码。通用代码应该包括两块:结果指示码、执行结果。其它语言一般都只能把结果指示码放入执行结果、在ExpectResult中用单独的timeout指示码指示是否超时,但rust则可以将其合并到结果指示码中,这样的话,处理时就可以统一处理:接收正确、接收错误、超时了。业务逻辑上更顺畅。

结果码我们可以如下定义:

#[derive(Clone, Copy)]
pub enum ExpectResultCode<T: Copy> {None,//框架不关心的业务码BPCode(T),//框架需要处理的超时,但也是业务层面的超时Timeout,
}

通过rust强大的枚举,原本由框架管理的Timeout就依然和BPCode一样具有业务层级的语义,但又不受业务代码的干扰。

然后我们可以根据我们的业务需要为其准备各种操作,但其中应该有两个:

impl ExpectResult {...其它业务代码...//代替new来创建期望结果的数据结构,如果不需要超时,就直接用newpub fn timeout(duration: Duration) -> Self {Self {expires_at: Instant::now() + duration,ready: false,waker: None,rc: ExpectResultCode::None,result: (...现在还没有结果,应该就是一堆None/0之类...),}}//异步操作完毕,设置结果;TC是业务结果码的泛型pub fn set_result(&mut self, rc: TC, result: ...) {self.rc = ExpectResultCode::BPCode(rc);self.result = ...;//异步操作结束self.ready = true;//唤醒等待中的poll任务if let Some(waker) = &self.waker {waker.wake();}}
}

然后我们就可以来实现Future了:

impl Unpin for ExpectResult {}
impl Future for ExpectResult {type Output = (ExpectResultCode, ...我们需要的结果类型...);fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {if self.ready {//异步操作执行完毕,即有人执行了set_resultPoll::Ready((self.rc, ...set_result函数送入的结果...))} else if self.expires_at < Instant::now() {//超时Poll::Ready((ExpectResultCode::Timeout, ...结果自然是没有了...))} else {//第一次调用,为上两个分支准备wakerlet waker: &core::task::Waker = cx.waker();//为set_result准备唤醒服务self.waker = Some(waker.clone());//为超时准备唤醒服务embassy_time_queue_driver::schedule_wake(self.expires_at.as_ticks(), waker);//最后,告诉系统:我先洗洗睡了,好了叫我Poll::Pending}}
}

ok,那该怎么用呢?!

//以各种方式创建一个ExpectResult并为其按业务需要装订数据
//如等待50毫秒
let es = ExpectResult::timeout(Duration::from_millis(50));
...其它操作,如通过串口发送命令...//准备完毕,异步操作已经启动,开始等待响应结果
//如你所见,对的,await针对的就是一个struct,而不是函数。
//啊?!可async修饰的就是一个函数啊?!不是async和await配合使用吗?!
//我们的ExpectResult已经在上面实现了Future,所以它可以直接用await进行串行化
//而如果是async函数,rust编译器会好心的给我们准备一个匿名struct的来实现我上面讲的东东的
//然后把async所修饰的函数作为需要等待执行完毕的异步操作丢进另一个线程/协程中执行
//并在该异步函数执行完毕时自动帮我们wake并返回该异步函数执行的结果
let (rc, ...我们需要的结果...) = es.await;
match rc {ExpectResultCode::Timeout => {//超时处理,如重发三次等等...},...
}

相比我们自己的实现,await机制,由于系统运行时和编译器配合着帮我们做了相当多的工作,自然非常轻便与灵活,而且编写起来也比较简单、逻辑清晰。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/783549.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何使用Axure RP制作网页原型并结合IIS服务实现公网访问本地HTML网页

文章目录 前言1.在AxureRP中生成HTML文件2.配置IIS服务3.添加防火墙安全策略4.使用cpolar内网穿透实现公网访问4.1 登录cpolar web ui管理界面4.2 启动website隧道4.3 获取公网URL地址4.4. 公网远程访问内网web站点4.5 配置固定二级子域名公网访问内网web站点4.5.1创建一条固定…

SSH免密登录——linux

SSH免密登录——linux 方法一一、用 ssh-key-gen 在本地主机上创建公钥和密钥二、用 ssh-copy-id 把客户端公钥追加到远程主机的 .ssh/authorized_key 上三、直接登录远程主机 方法二一、将生成的客户端公钥id_rsa.pub内容追加至目标主机.ssh/authorized_key 中参考链接 SSH免密…

动态规划-----背包类问题(0-1背包与完全背包)详解

目录 什么是背包问题&#xff1f; 动态规划问题的一般解决办法&#xff1a; 0-1背包问题&#xff1a; 0 - 1背包类问题 分割等和子集&#xff1a; 完全背包问题&#xff1a; 完全背包类问题 零钱兑换II: 什么是背包问题&#xff1f; 背包问题(Knapsack problem)是一种…

日历插件fullcalendar【笔记】

日历插件fullcalendar【笔记】 前言版权开源推荐日历插件fullcalendar一、下载二、初次使用日历界面示例-添加事件&#xff0c;删除事件 三、汉化四、动态数据五、前后端交互1.环境搭建-前端搭建2.环境搭建-后端搭建3.代码编写-前端代码fullcalendar.htmlfullcalendar.js 4.代码…

【更新】在湘源7、8中使用2023年11月国空用地用海分类

之前为了做控规&#xff0c;从湘源8中扒了一套国空用地用海的绘图参数给湘源7使用。 【预告】在湘源控规7中使用 国空用地用海分类标准 但是部里在2023年11月又发布了一套新的用地用海分类。 本想去湘源8里面再扒一下&#xff0c;结果发现湘源8自己还没有更新呢&#xff0c;…

free pascal:字符串模糊匹配库 FuzzyWuzzy 的编译过程

访问&#xff1a;pypi.org 搜索 fuzzywuzzy 访问&#xff1a;fuzzywuzzy PyPI 用鼠标滚动网页到底部&#xff0c;可见&#xff1a;Free Pascal: FuzzyWuzzy.pas (Free Pascal port) 下载 FuzzyWuzzy.pas-master.zip 后解压到当前目录。 cd D:\lazarus\projects\FuzzyWuz…

redis学习-redis配置文件解读

目录 1.单位说明 2. include配置 3. network网络配置 3.1 bind绑定ip配置 3.2保护模式protected-mode配置 3.3端口号port配置​编辑 3.4超时断开连接timeout配置 4. general通用配置 4.1守护进程模式daemonize配置 4.2进程id存放文件pidfile配置 4.3日志级别loglevel配置 4.…

实时数仓建设实践——滴滴实时数据链路组件的选型

目录 前言 一、实时数据开发在公司内的主要业务场景 二、实时数据开发在公司内的通用方案 三、特定场景下的实时数据开发组件选型 3.1 实时指标监控场景 3.2 实时BI分析场景 3.3 实时数据在线服务场景 3.4 实时特征和标签系统 四、各组件资源使用原则 五、总结和展望…

探索GPU的魔力:让你的计算速度翻倍,体验视觉盛宴

你是否曾为电脑运行速度慢而感到苦恼&#xff1f;是否渴望在游戏中体验更加逼真、流畅的画面&#xff1f;是否希望在深度学习任务中节省大量时间&#xff1f;那么&#xff0c;不妨让我向你介绍一种神奇的计算力量——GPU&#xff08;图形处理单元&#xff09;&#xff01; 立即…

通过 Docker 搭建 BookStack

文章目录 环境说明1、官方网站2、通过 Docker 部署总结 环境说明 操作系统版本&#xff1a;CentOS Linux release 7.9.2009 (Core) Docker 版本&#xff1a;Docker Engine - Community 24.0.2 BookStack 版本&#xff1a;23.02.3 MySQL 版本&#xff1a;8.0.32 1、官方网站 G…

浅读 Natural Language Generation Model for Mammography Reports Simulation

浅读 Natural Language Generation Model for Mammography Reports Simulation 这是一篇报告生成 去伪 的文章&#xff0c;重点看生成报告的 真实性 Abstract Extending the size of labeled corpora of medical reports is a major step towards a successful training of …

手搓 Docker Image Creator(DIC)工具(02):预备知识

此节主要简单介绍一下 Docker、Dockerfile 的基本概念&#xff0c;Dockerfile 对的基本语法&#xff0c;Windows 和 macOS 下 Docker 桌面的安装&#xff0c;Docker 镜像的创建和运行测试等。 1 关于 Docker Docker 是一个开源的应用容器引擎&#xff0c;它允许开发者打包应用…

【Effective Web】文件上传

文章目录 前言一、选择本地文件1.设计一个上传文件按钮2.FileReader读取文件内容 二、使用拖拽方式1.设计一个拖拽容器2.拖拽文件的相关事件回调 三、使用粘贴方式1.设计一个粘贴容器2.paste事件回调 四、总结 前言 前端无法像app一样直接操作本地文件&#xff0c;对本地文件的…

在Python中,当你执行 print(2, 3) 时

目录 在Python中,当你执行 print(2, 3) 时 在Python中,当你执行 print(2, 3) 时 你实际上是在调用 print 函数并传递给它两个参数:整数 2 和整数 3。print 函数会打印出这些参数,并在它们之间添加一个空格作为默认的分隔符。因此,输出将会是: 复制代码 2 3如果你希望打…

MTMT:构建比特币生态平行世界 打造铭文生态繁荣

近年来&#xff0c;随着铭文市场的火爆以及比特币ETF成功通过&#xff0c;比特币生态正经历着一场复兴&#xff0c;尤其是铭文市场作为新一代Web3的叙事&#xff0c;带来了全新的生产方式&#xff0c;可以预见&#xff0c;铭文就像流动性挖矿对于上一轮DeFi Summer的推动一样会…

vue watch 深度监听

vue2文档&#xff1a;API — Vue.js vue3文档&#xff1a;侦听器 | Vue.js watch 可以用来监听页面中的数据&#xff0c;但如果监听的源是对象或数组&#xff0c;则使用深度监听&#xff0c;强制深度遍历源&#xff0c;以便在深度变更时触发回调。 一&#xff0c;监听 <t…

蓝桥杯算法题-正则问题

问题描述 考虑一种简单的正则表达式&#xff1a; 只由 x ( ) | 组成的正则表达式。 小明想求出这个正则表达式能接受的最长字符串的长度。 例如 ((xx|xxx)x|(x|xx))xx 能接受的最长字符串是&#xff1a; xxxxxx&#xff0c;长度是 6。 输入格式 一个由 x()| 组成的正则表达式。…

ViveNAS性能调试笔记(一)

ViveNAS是一个开源的NAS文件服务软件&#xff0c;有一套独立自创的架构&#xff0c;ViveNAS希望能做到下面的目标&#xff1a; - 能支持混合使用高性能的介质(NVMe SSD)和低性能介质&#xff08;HDD&#xff0c;甚至磁带&#xff09;。做到性能、成本动态均衡。因此ViveNAS使用…

解锁背包问题:C++实现指南

文章目录 解锁背包问题&#xff1a;C实现指南01背包问题问题形式化动态规划解法C代码示例 完全背包问题动态规划解法C代码示例 结论 解锁背包问题&#xff1a;C实现指南 背包问题是计算机科学中的经典优化问题&#xff0c;常出现在算法研究和编程面试中。它是组合优化的一个例…

python 进程、线程、协程基本使用

1、进程、线程以及协程【1】进程概念【2】线程的概念线程的生命周期进程与线程的区别 【3】协程(Coroutines) 2、多线程实现【1】threading模块【2】互斥锁【3】线程池【4】线程应用 3、多进程实现4、协程实现【1】yield与协程【2】asyncio模块【3】3.8版本【4】aiohttp 1. 并发…