当前位置: 首页 > news >正文

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

相关文章:

rust嵌入式开发之await

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

UE4_碰撞_碰撞蓝图节点——Line Trace For Objects(对象的线条检测)

一、Line Trace For Objects&#xff08;对象的线条检测&#xff09;&#xff1a;沿给定线条执行碰撞检测并返回遭遇的首个命中&#xff0c;这只会找到由Object types指定类型的对象。注意他与Line Trace By Channel(由通道检测线条&#xff09;的区别&#xff0c;一个通过Obje…...

抽象类和接口的简单认识

目录 一、抽象类 1.什么是抽象类 2.抽象类的注意事项 3.抽象类与普通类的对比 二、接口 1.接口的简单使用 2.接口的特性 3.接口的使用案例 4.接口和抽象类的异同 一、抽象类 所谓抽象类&#xff0c;就是更加抽象的类&#xff0c;也就是说&#xff0c;这个类不能具体描…...

python-pytorch获取FashionMNIST实际图片标签数据集

在查看pytorch官方文档的时候&#xff0c;在这里链接中https://pytorch.org/tutorials/beginner/basics/data_tutorial.html的Creating a Custom Dataset for your files章节&#xff0c;有提到要自定义数据集&#xff0c;需要用到实际的图片和标签。 在网上找了半天没找到&a…...

深入探秘Python生成器:揭开神秘的面纱

一、问题起源&#xff1a; 想象一下&#xff0c;您掌握了一种魔法&#xff0c;在代码世界里&#xff0c;您可以轻松呼唤出一个整数。然而&#xff0c;事情并不总是看起来那样简单。在Python的奇妙王国中&#xff0c;我遇到了一个有趣的谜题&#xff1a; def tst():try:print(…...

红队攻防渗透技术实战流程:红队目标信息收集之批量信息收集

红队资产信息收集 1. 自动化信息收集1.1 自动化信息收集工具1.2 自动域名转换IP工具1.3 自动企业信息查询工具1.4 APP敏感信息扫描工具1.5 自动化信息工具的使用1.5.1 资产灯塔系统(ARL)1.5.1.1 docker环境安装1.2.2.9.1 水泽-信息收集自动化工具1. 自动化信息收集 1.1 自动化…...

【vue3学习笔记(二)】(第141-143节)初识setup;ref函数_处理基本类型;ref函数_处理对象类型

尚硅谷Vue2.0Vue3.0全套教程丨vuejs从入门到精通 本篇内容对应课程第141-143节 课程 P141节 《初识setup》笔记 1、setup是所有组合式API“表演的舞台”&#xff0c;组件中所用到的所有数据、方法、监视数据、生命周期钩子等都需要配置在setup中。 2、setup的两种返回值&…...

若依框架学习使用

若依官网项目拉取下来介绍 | RuoYi 项目运行&#xff1a; 1.idea安装&#xff0c;可以运行前后端 编辑器idea、jdk环境安装、数据库mysql、navicat工具、redis(redis-server启动)安装 2.navicat数据库连接, 创建数据库ry-vue并导入数据脚本ry_2021xxxx.sql&#xff0c;qua…...

蓝桥杯_数学模板

1.试除法判定质数 #include <iostream> using namespace std;bool is_zs(int x) {if(x<2) return false;for(int i2;i<x/i;i)if(x%i0)return false;return true; }int main() {int n; cin>>n;while(n--){int x; cin>>x;if(is_zs(x)) cout<<&quo…...

稀碎从零算法笔记Day31-LeetCode:接雨水

半月一去&#xff0c;望舒一轮&#xff0c;明天开始攻坚哈德题了 前言&#xff1a;非常经典的一道笔试题&#xff0c;看了保证血赚&#xff08;今天银泰星笔试第四题就是这个&#xff09; 题型&#xff1a;dp、模拟、双指针…… 链接&#xff1a;42. 接雨水 - 力扣&#xff…...

微前端的使用和注意事项 - qiankun

一、为什么使用微前端 微前端架构旨在解决单体应用在一个相对长的时间跨度下&#xff0c;由于参与的人员、团队的增多、变迁&#xff0c;从一个普通应用演变成一个巨石应用(Frontend Monolith)后&#xff0c;随之而来的应用不可维护的问题。微前端的核心目标是将巨石应用拆解成…...

uniapp微信小程序消息订阅详解

一、微信公众平台申请订阅模板 注意&#xff1a;订阅信息 这个事件 是 当用户 点击的时候触发 或者 是 支付成功后触发&#xff0c; 用户勾选 “总是保持以上选择&#xff0c;不再询问” 之后或长期订阅&#xff0c;下次订阅调用 wx.requestSubscribeMessage 不会弹窗&#xf…...

git 查看文件夹结构树

在Git中&#xff0c;没有直接的命令可以像文件系统那样展示一个可视化的文件结构树。但是&#xff0c;你可以使用一些外部工具或命令来达到这个目的。 以下是一些方法&#xff0c;你可以使用它们来查看Git仓库的文件结构树&#xff1a; 使用tree命令&#xff08;如果你的系统已…...

设计模式一详解

一、观察者模式 当一个对象状态发生改变时&#xff0c;依赖它的对象全部会收到通知&#xff0c;并自动更新 场景&#xff1a;一个事件发生后&#xff0c;要执行一连串更新操作。传统的编程方式&#xff0c;就是在事件的代码之后直接加入处理逻辑。当更新的逻辑增多之后&#x…...

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

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

SQLite3进行数据库各项常用操作

目录 前言1、SQLite介绍2、通过SQLite创建一个数据库文件3、往数据库文件中插入数据4、数据库文件信息查询5、修改数据库中的内容6、删除数据库中的内容 前言 本文是通过轻量化数据库管理工具SQLite进行的基础操作和一些功能实现。 1、SQLite介绍 SQLite是一个广泛使用的嵌入…...

Debian GNU/Linux 安装docker与docker compose

安装 Docker 更新包列表 sudo apt update 安装必要的软件包&#xff0c;以便让 APT 可以通过 HTTPS 使用存储库&#xff1a; sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common 添加 Docker 的官方 GPG 密钥&#xff1a; cu…...

图片标注编辑平台搭建系列教程(2)——fabric.js简介

文章目录 综述数据管理图形渲染图形编辑事件监听预告 综述 fabric提供了二维图形编辑需要的所有基础能力&#xff0c;包括&#xff1a;数据管理、图形渲染、图形编辑和事件监听。其中&#xff0c;图形编辑可以通过事件监听和图形渲染来实现&#xff0c;所以可以弃用。数据管理…...

Debian linux版本下运行的openmediavault网盘 千兆网卡升级万兆

一、适用场景 1、使用vmware ESXi虚拟化平台运行多种不同应用服务器时&#xff0c;其中网盘服务器采用开源的openmediavault搭建&#xff1b; 2、将老专业服务器升级千兆网为万兆网&#xff1b; 3、需要转移的数据量大的企业或用户&#xff1b; 4、从服务器到服务器的数据转移…...

前端 CSS 经典:grid 栅格布局

前言&#xff1a;Grid 布局是将容器划分成"行"和"列"&#xff0c;产生单元格&#xff0c;然后将"项目"分配给划分好的单元格&#xff0c;因为有行和列&#xff0c;可以看作是二维布局。 一 术语 1. 容器 采用网格布局的区域&#xff0c;也就是…...

rknn优化教程(二)

文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK&#xff0c;开始写第二篇的内容了。这篇博客主要能写一下&#xff1a; 如何给一些三方库按照xmake方式进行封装&#xff0c;供调用如何按…...

c++ 面试题(1)-----深度优先搜索(DFS)实现

操作系统&#xff1a;ubuntu22.04 IDE:Visual Studio Code 编程语言&#xff1a;C11 题目描述 地上有一个 m 行 n 列的方格&#xff0c;从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子&#xff0c;但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

python如何将word的doc另存为docx

将 DOCX 文件另存为 DOCX 格式&#xff08;Python 实现&#xff09; 在 Python 中&#xff0c;你可以使用 python-docx 库来操作 Word 文档。不过需要注意的是&#xff0c;.doc 是旧的 Word 格式&#xff0c;而 .docx 是新的基于 XML 的格式。python-docx 只能处理 .docx 格式…...

大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计

随着大语言模型&#xff08;LLM&#xff09;参数规模的增长&#xff0c;推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长&#xff0c;而KV缓存的内存消耗可能高达数十GB&#xff08;例如Llama2-7B处理100K token时需50GB内存&a…...

rnn判断string中第一次出现a的下标

# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...

LLMs 系列实操科普(1)

写在前面&#xff1a; 本期内容我们继续 Andrej Karpathy 的《How I use LLMs》讲座内容&#xff0c;原视频时长 ~130 分钟&#xff0c;以实操演示主流的一些 LLMs 的使用&#xff0c;由于涉及到实操&#xff0c;实际上并不适合以文字整理&#xff0c;但还是决定尽量整理一份笔…...

书籍“之“字形打印矩阵(8)0609

题目 给定一个矩阵matrix&#xff0c;按照"之"字形的方式打印这个矩阵&#xff0c;例如&#xff1a; 1 2 3 4 5 6 7 8 9 10 11 12 ”之“字形打印的结果为&#xff1a;1&#xff0c;…...

node.js的初步学习

那什么是node.js呢&#xff1f; 和JavaScript又是什么关系呢&#xff1f; node.js 提供了 JavaScript的运行环境。当JavaScript作为后端开发语言来说&#xff0c; 需要在node.js的环境上进行当JavaScript作为前端开发语言来说&#xff0c;需要在浏览器的环境上进行 Node.js 可…...

react更新页面数据,操作页面,双向数据绑定

// 路由不是组件的直接跳转use client&#xff0c;useEffect&#xff0c;useRouter&#xff0c;需3个结合&#xff0c; use client表示客户端 use client; import { Button,Card, Space,Tag,Table,message,Input } from antd; import { useEffect,useState } from react; impor…...

react-pdf(pdfjs-dist)如何兼容老浏览器(chrome 49)

之前都是使用react-pdf来渲染pdf文件&#xff0c;这次有个需求是要兼容xp环境&#xff0c;xp上chrome最高支持到49&#xff0c;虽然说iframe或者embed都可以实现预览pdf&#xff0c;但为了后续的定制化需求&#xff0c;还是需要使用js库来渲染。 chrome 49测试环境 能用的测试…...