Rust 学习心得<3>:无栈协程

2023-10-30

Rust作为一门新兴语言,主打系统编程,提供了多种编写代码的模式。Rust在2019年底正式推出了 async/await语法,标志着Rust也进入了协程时代。下面让我们来看一看。Rust协程和Go协程究竟有什么不同。

有栈协程 vs. 无栈协程

协程的需求来自于C10K问题,这里不做更多探讨。早期解决此类问题的办法是依赖于操作系统提供的I/O复用操作,也就是 epoll/IOCP 多路复用加线程池技术来实现的。本质上这类程序会维护一个复杂的状态机,采用异步的方式编码,消息机制或者是回调函数。很多用 C/C++ 实现的框架都是这个套路,缺点在于这样的代码一般比较复杂,特别是异步编码加状态机的模式对于程序员是一个很大的挑战。但是从另外一个角度看,符合人类逻辑思维的操作方式却恰恰是同步的。

考虑一个web server的场景:每次一个连接一般是请求下载一些数据,如果可以用一个线程来处理每一次新连接,那么这个内部的代码逻辑就可以用同步的方式一路写下来:首先接收数据,然后完成HTTP request解析。根据HTTP头部的信息访问数据库,然后将取得的结果封装在HTTP response中,返回给用户,最后关闭连接。如果是这样,你会发现这里并不需要状态机,也没有什么回调函数,很可能也不需要定时器,整个的过程就是一个流水账,而这正是人类最容易理解的思维方式。然而,我们不能简单地用多线程来解决C10K问题,因为操作系统的线程资源是很有限的,而且是昂贵的。操作系统会限制可以打开的线程数,同时线程之间的切换开销也是比较大的。

Go 有栈协程

Go语言的出现提供了一种新的思路。Go语言的协程则相当于提供了一种很低成本的类似于多线程的执行体。在Go语言中,协程的实现与操作系统多线程非常相似。操作系统一般使用抢占的方式来调度系统中的多线程,而Go语言中,依托于操作系统的多线程,在运行时刻库中实现了一个协作式的调度器。这里的调度真正实现了上下文的切换,简单地说,Go系统调用执行时,调度器可能会保存当前执行协程的上下文到堆栈中。然后将当前协程设置为睡眠,转而执行其他的协程。这里需要注意,所谓的Go系统调用并不是真正的操作系统的系统调用,而是Go运行时刻库提供的对底层操作系统调用的一个封装。举例说明:Socket recv。我们知道这是一个系统调用,Go的运行时刻库也提供了几乎一模一样的调用方式,但这只是建立在 epoll 之上的模拟层,底层的socket是工作在非阻塞的方式,而模拟层提供给我们了看上去是阻塞模式的socket。读写这个模拟的socket会进入调度器,最终导致协程切换。目前Go调度器实现在用户空间,本质上是一种协作式的调度器。这也是为什么如果写了一个死循环在协程里,则协程永远没有机会被换出,一个Processor相当于就被浪费掉了。

有栈的协程和操作系统多线程是很相似的。考虑以下伪代码:

func routine() int
{
	var a = 5
	sleep(1000)
	a += 1
	return a
}

sleep调用时,会发生上下文的切换,当前的执行体被挂起,直到约定的时间再被唤醒。局部变量a 在切换时会被保存在栈中,切换回来后从栈中恢复,从而得以继续运行。所谓有栈就是指执行体本身的栈。每次创建一个协程,需要为它分配栈空间。究竟分配多大的栈的空间是一个技术活。分的多了,浪费,分的少了,可能会溢出。Go在这里实现了一个协程栈扩容的机制,相对比较优雅的解决了这个问题。另外一个问题,关于上下文切换,这一般是跟平台或者CPU相关的代码,因为要涉及到寄存器操作。同时上下文切换也是有一点代价的,因为毕竟需要额外执行一些指令(个人觉得这一点可以忽略掉,无栈的协程实现难道不是也需要一些额外的指令来完成程序逻辑的跳转?)。

有栈协程看起来还是比较直观,特别是对于开发人员比较友好。如果对比一下Rust实现的无栈协程,就会知道因为引入这个栈,保存上下文,从而解决了很多很麻烦的问题。

关于Go,讲一点题外话。

Go有一个比较庞大的运行时刻库。从上文我们了解到,因为Go调度器的需要,运行时刻库把所有的系统调用都做了封装,这些所谓系统调用都被引入了调度器的调度点,也就是说,执行这类系统调用会进行协程的上下文切换。所以换一句话说。Go的系统调用,其实都是被包装过的,能够感知协程的系统调用。所以从这个角度也可以理解为什么Go的运行时刻库是比较庞大的。另外,cgo的执行也是类似的过程。因为调用的C代码非常有可能通过C库来执行系统调用,这样会使线程进入阻塞,从而影响Go的调度器的行为。所以我们看到cgo总会执行entersyscallexitsyscall,就是这个原因。

Rust 协程

绿色线程 GreenThread

早期的Rust支持一个所谓的绿色线程,其实就是有栈协程的实现,与Go协程实现很相似。在0.7之后,绿色线程就被删除了。其中一个原因是,如果引入这样的机制,那么运行时刻库也必须如Go语言一样能够支持有栈协程,也就是之前讨论Go题外话提到的内容。Go没有Native thread的概念,语言层面只支持协程,选择封装全部的系统调用很合理。然而,如果Rust也打算这么做,那么Native thread和协程运行库API统一的问题将很难解决。

无栈协程

无栈协程顾名思义就是不使用栈和上下文切换来执行异步代码逻辑的机制。这里异步代码虽然是异步的,但执行起来看起来是一个同步的过程。从这一点上来看Rust协程与Go协程也没什么两样。举例说明:

async fn routine() 
{
	let mut a = 5;
	sleep(1000).await;
	a = a + 1;
	a
}

几乎是一样的流程。Sleep会导致睡眠,当时间已到,重新返回执行,局部变量a 内容应该还是5。Go协程是有栈的,所以这个局部变量保存在栈中,而Rust是怎么实现的呢?答案就是 Generator 生成的状态机。Generator 和闭包类似,能够捕获变量a,放入一个匿名的结构中,在代码中看起来是局部变量的数据 a,会被放入结构,保存在全局(线程)栈中。另外值得一提的是,Generator 生成了一个状态机以保证代码正确的流程。从sleep.await 返回之后会执行 a=a+1 这行代码。async routine() 会根据内部的 .await 调用生成这样的状态机,驱动代码按照既定的流程去执行。

按照一般的说法。无栈协程有很多好处。首先不用分配栈。因为究竟给协程分配多大的栈是个大问题。特别是在32位的系统下,地址空间是有限的。每个协程都需要专门的栈,很明显会影响到可以创建的协程总数。其次,没有上下文切换,貌似性能也许会好一些?当然,更大的好处是并不需要与CPU体系相关代码,也就有了更好的跨平台的能力。当然,无栈问题也不少。例如,Rust著名的PIN问题。另外,个人觉得Rust的无栈协程主要问题是不那么直观,理解起来会稍微吃力一些。

协程解决的问题

Rust语言真正实现 async/await 语法只是去年底的事情。在那之前,有一些其他临时使用宏的替代做法。所以现在去看一些开源的软件项目,真正采用 await 写代码还是很少的,主要是 poll 的方式,这样的代码需要自己维护各种状态。一个经典的例子就是Sink发送的三件套:poll_ready/start_send/poll_flush,首先需要检查是否缓冲区有待发送的数据,若是,则优先处理这一部分数据。然后检查底层是否就绪,否则无法发送,这时候需要把当前发送的东西转存下来,也就是前面提到的发送缓冲区。如果用C语言写过epoll 相关的代码,那么会发现和这里也没有什么大的区别。因为这就是异步编程大致的模式。而事实上,如果可以用await来写代码,直接调用SinkExt的send().await方法,一切烦恼都消失了。SinkExt::send 内部实现了包含发送缓冲的Sink的三件套,而await 用一种简洁的方式将这一切优雅地呈现出来。这种利用.await 写出来的代码,看似是用同步的方式在做异步的编程,比较简洁,易于理解。

总之,个人觉得Rust异步编程的未来是 await。早期手动来写各种poll方法,实在是太繁琐了。语言实则是一种工具,被发明出来是用来帮助程序员的,而不是造成更多的负担。我相信这也是Rust .await 最大的意义。

下一篇文章,我们来研究下 async/await 究竟做了什么。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Rust 学习心得<3>:无栈协程 的相关文章

随机推荐

  • C0206 [2011普及组-A]数字反转(C语言写)

    题目描述 给定一个整数 请将该数各个位上数字反转得到一个新数 新数也应满足整数的常见形式 即除非给定的原数为零 否则反转后得到的新数的最高位数字不应为零 参见样例2 输入描述 共一行 一个整数N 输出描述 共一行 一个整数 表示反转后的新数
  • @Cacheable缓存注解(以Redis作为缓存)

    使用时需要先导入依赖包
  • 2022华为杯C题汽车制造涂装-总装缓存调序区调度优化问题建模解决

    一 背景介绍 汽车制造厂主要由焊装车间 涂装车间 总装车间构成 每个车间有不同的生产偏好 如 焊装车间由于车身夹具的限制偏向最小车型及配置切换生产 涂装车间由于喷漆 固定每5辆车清洗喷头 颜色切换也需清洗喷头 限制偏向颜色以5的倍数切换生产
  • 刷脸支付抓住机会将财富收入囊中

    目前刷脸支付很多地方都已经开始落地商业 2019年相比支付行业最火爆的项目应该就是刷脸支付代理了 相信很多消费者也都体验到了刷脸支付带给我们的便利性和智能化的体验 而对于商家是大大节省人力和时间成本 加盟刷脸支付项目有很大的商机和发展前景
  • 信号完整性分析:关于传输线的三十个问题解答(二)

    11 对于 50 欧姆带状线的纵横比 什么是好的经验法则 What is a good rule of thumb for the aspect ratio of a 50 Ohm stripline 在带状线几何形状和 FR4 基板中 线
  • 信息度量——熵

    1 熵 1 1 熵的定义和理解 热力学用熵值描述系统混乱程度或不确定程度 香农用信息熵的概念来描述信源的不确定度 信息量与信息熵是相对的 告诉你一件事实 你获取了信息量 但减少了熵 或者说 得知一件事实后信息熵减少的量 就是你得到的这个事实
  • 例题讲解拉格朗日乘子法、线性可分支持向量机(SVM)的推导

    支持向量机 Support Vector Machine SVM 于1995年被首次提出 在解决小样本 非线性及高维度模式识别模式中具有许多特有的优势 1 SVM的相关概念 在介绍SVM之前需要了解一些相关概念 最优分类超平面 分类超平面方
  • flutter 使用image_picker上传图片

    第一步 封装 可以单独放在一个文件里 可以直接复制 选择图片函数 拍照 HspTakePhoto async var image await ImagePicker pickImage source ImageSource camera m
  • React 全栈体系(六)

    第三章 React 应用 基于 React 脚手架 二 组件的组合使用 TodoList 3 添加 todo 3 1 App src App jsx 创建 外壳 组件App import React Component from react
  • 后端返回JSON数据格式,前端根据JSON数据 导出.CSV文件

    以下仅供参考 效果图 前端JSON导出CSV文件 param Object dataObj 对象 title 名称 jsonKey Name 键值对 key data JSON数据 fileName 文件名 function exportC
  • Java中的OIO和NIO详解(含代码)

    简介及示例 Java NIO New I O 和OIO Old I O 是Java提供的两种不同的I O模型 OIO Old I O 是传统的阻塞I O模型 也称为同步I O 在OIO模型中 每个I O操作 如读写操作 都会阻塞当前线程 直
  • 随手记录(日历)

    日历
  • 7.最大最小距离算法与最大最小距离

    7 最大最小距离算法与最大最小距离 最大最小距离算法 最大最小距离算法是一种聚类算法 算法描述 1 任意选取一个样本模式作为第一聚类中心K1 2 选择离Z1最远欧氏距离的模式样本作为第二聚类中心K2 3 逐个计算每个模式样本与已确定的所有聚
  • 哈希表(散列表)原理详解

    什么是哈希表 哈希表 Hash table 也叫散列表 是根据关键码值 Key value 而直接进行访问的数据结构 也就是说 它通过把关键码值映射到表中一个位置来访问记录 以加快查找的速度 这个映射函数叫做散列函数 存放记录的数组叫做散列
  • Kibana启动Kibana server is not ready yet

    问题 页面访问Kibana路径显示 Kibana server is not ready yet 原因1 启动Kibana时指定ElasticSearch地址错误 http 116 62 19 81 9200 需要改为自己本机服务器的ip和
  • python调用GPT实现:智能用例生成工具

    工具作用 根据输入的功能点 生成通用测试点 实现步骤 工具实现主要分2个步骤 1 https请求调用Gpt 将返回响应结果保存为 md文件 2 用python实现 将 md文件转换成 xmind文件 3 写个简单的前端页面 调用上述步骤接口
  • zabbix-server仪表板出现: 不

    1 检查配置文件 vi etc zabbix zabbix server conf 里面的配置项是否还是原始的 如果是 请修改如下 2 检查第二个配置文件 vi etc zabbix web zabbix conf php 修改之前的原始配
  • 未转变者怎么调服务器难度,Unturned——作弊模式下的各项数值微调【较实用的已详细描述】...

    您尚未登录 立即登录享受更好的浏览体验 您需要 登录 才可以下载或查看 没有帐号 注册 register x 本帖最后由 Crazy Zombie 于 2017 8 11 10 31 编辑 如标题所示 在下发一个关于Unturned模式下各
  • 区块链与哈希函数

    目录 哈希函数 定义 性质 发展 常见攻击方法 1 穷举攻击 2 生日攻击 3 其他攻击 构造方法 1 利用对称密码体制来设计哈希函数 2 直接设计哈希函数 编辑 常用哈希函数简介 1 SHA 256算法 编辑 2 Keccak算法 3 S
  • Rust 学习心得<3>:无栈协程

    Rust 学习心得 lt 3 gt 无栈协程 有栈协程 vs 无栈协程 Go 有栈协程 Rust 协程 绿色线程 GreenThread 无栈协程 协程解决的问题 Rust作为一门新兴语言 主打系统编程 提供了多种编写代码的模式 Rust在