【投稿】刀哥:Rust学习笔记 5

2024-06-23 00:08
文章标签 刀哥 rust 投稿 笔记 学习

本文主要是介绍【投稿】刀哥:Rust学习笔记 5,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

@[TOC](Rust 学习心得<5>:异步代码的几种写法)

Rust历史不长,仍然处于快速发展的历程中。关于异步编程的模式,现在已经发展到async/await协程的高级阶段。大概是因为async/await出现的时间还不长,所以现有大多数的开源项目并不是或不是纯粹使用async/await来书写的,而是前前后后有多种的写法。这样的状况给Rust的学习带来了一些的难度。在这里,我们来捋一捋异步代码的几种写法。

mio

最原始的方式是使用mio进行开发。mio是一个底层异步I/O库,提供非阻塞方式的API,具有很高的性能。实际上mio是对于操作系统epoll/kqueue/IOCP的封装。在C/C++中我们使用libevent之类的库,mio可以理解为对应的Rust版本。基于mio的代码大致如下:

 loop {// Poll Mio for events, blocking until we get an event.poll.poll(&mut events, None)?;// Process each event.for event in events.iter() {if event.is_writable() {// socket可写,开始发送数据}if event.is_readable() {// socket可读,开始接收数据}// socket 关闭,退出循环return Ok(());}}

总的来说,这是完全基于异步事件通知的写法,和C/C++区别不是很大,异步代码对于程序员是一个挑战,当代码逻辑越来越复杂,添加新功能或是解决已有问题的难度也越来越大。

另外,mio实现的是一个单线程事件循环,虽然可以处理成千上万路的I/O操作,但没有多线程的能力,需要自己扩充。

Future Poll

为了更好地规范异步的逻辑,Rust抽象出Future表示尚未发生的事物。这些Future可以用很多方式組合成一个更复杂的复合Future来代表一系列的事件。Future需要程序主动去poll(轮询)才能获取到最终的结果,每一次轮询的结果可能是Ready或者Pending

运行库提供ExecutorReactor来执行Future,也就是调用Futurepoll方法循环执行一系列就绪的Future,当Future返回Pending的时候,会将Future转移到Reactor上等待唤醒。Reactor被用来负责唤醒之前无法完成的Future。事实上,tokioReactor是基于mio实现的,而async-std/smol则是封装了epoll/kqueue/IOCP,提供类似的功能。

手动实现Future是一件相对繁琐的工作,主要的问题在于异步模式本身的特性。例如,接收网络数据,无法臆测每次轮询会收到多少字节的数据,往往需要开辟一段接收缓冲区容纳数据,协议解码也需要一个状态机拼包向上层提交;发送网络数据存在相似问题,发送数据时底层未就绪,则缓冲发送数据,待下次轮询时,需要首先检查并处理发送缓冲区。另外还有一些值得注意的地方,如果手动实现的Future返回Pending,则必须自己实现唤醒机制,也就是需要将cx克隆一份记下来,然后在适当的时侯调用cx.wake()。因为网络相关的功能往往是分层的,因此手动的Poll循环也会是层层堆叠的,这时候,返回值Poll::Ready(T)就有学问了。泛型T可能包裹各种不同的数据,Option<T>Result<T,E>,或者两者的组合。因为最外层还有一个Poll<T>,所有这时候的match语句写起来会非常臃肿,粘贴复制写很多代码,完成的功能却非常有限,而且由于这些代码很相似,大大增加了出错的可能性。

标准库中仅仅定义了Future,更多的相关功能需要引用futures-rs类库,里面定义了一系列有关异步的操作,包括StreamSinkAsyncReadAsyncWrite等基础Trait,以及对应实现了大量方便操作的组合子的Ext Trait,特别用途的fusedBoxTry系列的扩展,诸如join!select!pin_mut!等一系列的宏。理论上,不使用这些扩展也能写出代码,只不过那样的代码很可能篇幅会长的可怕。值得一提的是,除了一些可以简化代码的过程宏之外,扩展Trait提供的组合子也会让代码精简不少。比如Future::and_then可以让代码写成链式调用的方式;Sink::send包装了Sink发送三步骤 poll_ready/start_send/poll_flush,使用.await一行代码直接就可以完成发送。因此,很多poll方式的代码实际上是准确地说是混合式的,其中也使用了不少async代码块。

总之,搞清楚Future相关的这些内容是需要花费不少时间,更不用说用它们来写代码了。不过,即便是使用async/await这种更高级原语,也是有必要了解底层的工作原理和实现机制,所谓知其然知其所以然。

async/await

使用async/await可以将异步的代码写得类似同步的过程,更加符合人体工程学。因为async被翻译为一个Future状态机,原先在poll方式中需要处理的与Pending相关的状态现在都由async生成的状态机自动完成,因此大大减轻了程序员的心智负担。

如前所述,底层的Futures提供了很多方便的组合子扩展Future,使用起来很简洁,可以极大地简化代码。例如,上文提到过的Sink::send包装了发送缓冲区的实现和异步发送的三个步骤;AsyncRead::read_exact实现了读取指定字节数的功能,在处理网络协议解析时可以避免手写一个拼包状态机;AsyncWrite::write_all实现了发送全部数据以及发送缓冲,等等。正是在这些底层功能的支持下,async/await成为了更高级的书写异步代码的方式。也许会有少许担心,这样所谓“高级”会不会在性能上有很大损失?笔者个人不这么认为。自动实现的状态机也许未必比程序员手动完成的性能更差。状态机编程对于任何人,即便是一个有经验的程序员都是不小挑战。蹩脚的状态机实现不仅可能有性能问题,更大的风险来自于实现上的漏洞,以及维护上的困难。代码写出来更多是给别人看的,完成同样的功能,简洁的代码更有可能是更高质量的代码。

以下例子是固定长度分割的报文接收过程,使用async/await是很简单的。如果实现为一个Stream/poll_next,代码会复杂很多。

    /// convenient method for reading a whole framepub async fn recv_frame(&mut self) -> io::Result<Vec<u8>> {let mut len = [0; 4];let _ = self.inner.read_exact(&mut len).await?;  // inner socket, 支持 AsyncReadlet n = u32::from_be_bytes(len) as usize;if n > self.max_frame_len {let msg = format!("data length {} exceeds allowed maximum {}",n, self.max_frame_len);return Err(io::Error::new(io::ErrorKind::PermissionDenied, msg));}let mut frame = vec![0; n];self.inner.read_exact(&mut frame).await?;Ok(frame)}

最后,完全使用async/await写代码目前还有几个问题:

async trait

当前Trait 不支持 async fn,无法直接用Trait来抽象异步方法。暂时解决办法是使用三方库 async-trait。如下:

use async_trait::async_trait;#[async_trait]
trait Advertisement {async fn run(&self);
}

async_trait将代码转换为一个返回 Pin<Box<dyn Future + Send + 'async>> 的同步方法。因为装箱和动态派发的原因,性能上会有少许损失。

异步析构

当前drop方法必须是同步调用,不能使用await语法。当一个I/O对象越过生命周期被析构,往往在关闭底层句柄之前,还需要完成某些I/O操作。比如,通知网络对端连接已经关闭。在同步代码中,我们只需要在drop()中置入这些操作,但是在异步代码中,无法在drop()中做类似的事情。

解决办法,总是在异步I/O对象越过生命周期之前显式地执行关闭动作,或是,实现一个类似GC的功能,专门负责清理工作。

展望

笔者在学习Rust过程中,主要关注网络相关的并发编程。因为之前有在Go版本的ipfs/libp2p上的开发经验,故而学习研究了rust-libp2p以及nervos tentaclerust-libp2pParity实现的准官方版本,但是这个项目的代码及其难懂,过于强调使用泛型参数的抽象,导致代码可读性非常差。请教了代码作者,他承认代码可能有些复杂,但也强调都是有原因的... nervos tentacle的实现在协议上不够完整,特别是与标准libp2p并不兼容。两个项目共有的特点是主要用poll的方式写代码,逻辑上都是状态机的嵌套。

因此,笔者试图完全使用async/await方式重构libp2p,参考rust-libp2p的实现,代码协程化,向上层提供纯粹的异步接口,争取在API层面的体验接近go-libp2p,这是推广Rust协程机制的一个尝试,同时也是个人的一个学习的过程。目前刚刚起步,仅完成了secioyamux部分,待合适时机开源,期望更多Rust爱好者共同来开发完善。

参考:Asynchronous Destructors

这篇关于【投稿】刀哥:Rust学习笔记 5的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1085795

相关文章

51单片机学习记录———定时器

文章目录 前言一、定时器介绍二、STC89C52定时器资源三、定时器框图四、定时器模式五、定时器相关寄存器六、定时器练习 前言 一个学习嵌入式的小白~ 有问题评论区或私信指出~ 提示:以下是本篇文章正文内容,下面案例可供参考 一、定时器介绍 定时器介绍:51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成。 定时器作用: 1.用于计数系统,可

问题:第一次世界大战的起止时间是 #其他#学习方法#微信

问题:第一次世界大战的起止时间是 A.1913 ~1918 年 B.1913 ~1918 年 C.1914 ~1918 年 D.1914 ~1919 年 参考答案如图所示

[word] word设置上标快捷键 #学习方法#其他#媒体

word设置上标快捷键 办公中,少不了使用word,这个是大家必备的软件,今天给大家分享word设置上标快捷键,希望在办公中能帮到您! 1、添加上标 在录入一些公式,或者是化学产品时,需要添加上标内容,按下快捷键Ctrl+shift++就能将需要的内容设置为上标符号。 word设置上标快捷键的方法就是以上内容了,需要的小伙伴都可以试一试呢!

Tolua使用笔记(上)

目录   1.准备工作 2.运行例子 01.HelloWorld:在C#中,创建和销毁Lua虚拟机 和 简单调用。 02.ScriptsFromFile:在C#中,对一个lua文件的执行调用 03.CallLuaFunction:在C#中,对lua函数的操作 04.AccessingLuaVariables:在C#中,对lua变量的操作 05.LuaCoroutine:在Lua中,

AssetBundle学习笔记

AssetBundle是unity自定义的资源格式,通过调用引擎的资源打包接口对资源进行打包成.assetbundle格式的资源包。本文介绍了AssetBundle的生成,使用,加载,卸载以及Unity资源更新的一个基本步骤。 目录 1.定义: 2.AssetBundle的生成: 1)设置AssetBundle包的属性——通过编辑器界面 补充:分组策略 2)调用引擎接口API

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

大学湖北中医药大学法医学试题及答案,分享几个实用搜题和学习工具 #微信#学习方法#职场发展

今天分享拥有拍照搜题、文字搜题、语音搜题、多重搜题等搜题模式,可以快速查找问题解析,加深对题目答案的理解。 1.快练题 这是一个网站 找题的网站海量题库,在线搜题,快速刷题~为您提供百万优质题库,直接搜索题库名称,支持多种刷题模式:顺序练习、语音听题、本地搜题、顺序阅读、模拟考试、组卷考试、赶快下载吧! 2.彩虹搜题 这是个老公众号了 支持手写输入,截图搜题,详细步骤,解题必备

《offer来了》第二章学习笔记

1.集合 Java四种集合:List、Queue、Set和Map 1.1.List:可重复 有序的Collection ArrayList: 基于数组实现,增删慢,查询快,线程不安全 Vector: 基于数组实现,增删慢,查询快,线程安全 LinkedList: 基于双向链实现,增删快,查询慢,线程不安全 1.2.Queue:队列 ArrayBlockingQueue:

硬件基础知识——自学习梳理

计算机存储分为闪存和永久性存储。 硬盘(永久存储)主要分为机械磁盘和固态硬盘。 机械磁盘主要靠磁颗粒的正负极方向来存储0或1,且机械磁盘没有使用寿命。 固态硬盘就有使用寿命了,大概支持30w次的读写操作。 闪存使用的是电容进行存储,断电数据就没了。 器件之间传输bit数据在总线上是一个一个传输的,因为通过电压传输(电流不稳定),但是电压属于电势能,所以可以叠加互相干扰,这也就是硬盘,U盘

人工智能机器学习算法总结神经网络算法(前向及反向传播)

1.定义,意义和优缺点 定义: 神经网络算法是一种模仿人类大脑神经元之间连接方式的机器学习算法。通过多层神经元的组合和激活函数的非线性转换,神经网络能够学习数据的特征和模式,实现对复杂数据的建模和预测。(我们可以借助人类的神经元模型来更好的帮助我们理解该算法的本质,不过这里需要说明的是,虽然名字是神经网络,并且结构等等也是借鉴了神经网络,但其原型以及算法本质上还和生物层面的神经网络运行原理存在