当前位置: 首页 > 后端技术 > Python

无栈协程-Rust学习笔记

时间:2023-03-26 17:28:18 Python

作者:谢经纬,人称“刀哥”,20年IT从业者,数据通信网络专家,电信网络架构师,现任Netwarps开发总监。刀哥在操作系统、网络编程、高并发、高吞吐量、高可用等领域有多年的实践经验,对网络和编程方面的新技术有着浓厚的兴趣。作为一门新兴语言,Rust专注于系统编程。提供多种编写代码的模式。2019年底,async/await语法正式上线,标志着Rust也进入了协程时代。下面我们一起来看看吧。Rust协程和Go协程有什么区别?Stackedcoroutinesvs.Stacklesscoroutines对协程的需求来自于C10K问题,这里不再赘述。早期解决此类问题的方法是依赖操作系统提供的I/O多路复用操作,即epoll/IOCP多路复用加线程池技术。本质上,这种程序会维护一个复杂的状态机,以异步方式编码,消息机制或回调函数。许多用C/C++实现的框架都使用这个例程。缺点是这样的代码一般比较复杂,尤其是异步编码加状态机的模式对程序员来说是一个很大的挑战。但从另一个角度来看,符合人类逻辑思维的运行方式恰恰是同步的。考虑一个web服务器场景:每个连接通常会请求下载一些数据,如果可以用一个线程来处理每个新的连接,那么内部代码逻辑就可以一路写成同步的方式:先接收数据,再完成HTTP请求解析。根据HTTP头中的信息访问数据库,然后将得到的结果封装在HTTP响应中,返回给用户,最后关闭连接。如果是这样的话,你会发现不需要状态机,不需要回调函数,很可能也不需要定时器。整个过程是一个流水账,这是人类最容易理解的思维方式。但是,我们不能简单地使用多线程来解决C10K问题,因为操作系统的线程资源是有限的,而且是昂贵的。操作系统会限制可以开启的线程数,线程之间的切换开销比较大。Go栈协程的出现为Go语言提供了一种新的思路。Go语言的协程相当于提供了一个类似于多线程的非常低成本的执行体。在Go语言中,协程的实现与操作系统多线程非常相似。操作系统一般采用抢占式的方式来调度系统中的多线程,但是在Go语言中,依托操作系统的多线程,在运行时库中实现了一个协同调度器。这里的调度真正实现了上下文切换。简单的说,当执行Go系统调用时,调度器可能会将当前正在执行的协程的上下文保存到栈中。然后将当前协程设置为休眠并改为执行其他协程。这里需要注意的是,所谓的Go系统调用并不是真正的操作系统系统调用,而是Go运行时库提供的对底层操作系统调用的封装。例如:套接字接收。我们知道这是一个系统调用,Go的运行时库也提供了几乎相同的调用方式,但这只是建立在epoll之上的一个仿真层。底层套接字以非阻塞方式工作,仿真层提供我们有一个看似处于阻塞模式的套接字。读写这个模拟socket会进入调度器,最终导致协程切换。目前,Go调度器是在用户空间实现的,本质上是一个协作式调度器。这就是为什么如果在协程中写死循环,协程永远没有机会被换出,一个Processor就相当于被浪费了。带堆栈的协程与操作系统多线程非常相似。考虑如下伪代码:funcroutine()int{vara=5sleep(1000)a+=1returna}当调用sleep时,会发生上下文切换,当前执行体会被挂起,直到约定时间恢复唤醒。局部变量a在切换时会保存在栈中,切换回来后从栈中恢复,这样才能继续运行。所谓栈,就是指执行体本身的栈。每次创建协程,都需要为其分配栈空间。分配多少堆栈空间是一项技术活动。分多了,就浪费了;如果划分得太少,它可能会溢出。Go在这里实现了协程栈扩展机制,比较优雅的解决了这个问题。另外一个问题,关于上下文切换,这个一般是跟平台或者CPU相关的代码,因为涉及到寄存器操作。同时,上下文切换也是有代价的,因为毕竟需要执行一些额外的指令(我个人认为这个可以忽略,stackless协程的实现是否也需要额外的指令来完成程序的跳转逻辑?)。堆叠协程看起来更直观,对开发人员特别友好。如果你对比过Rust实现的stackless协程,你就会知道,因为引入了这个stack和context的保存,解决了很多麻烦的问题。关于Go,稍微扯点题外话。Go有一个比较大的运行时库。从上面我们了解到,因为Go调度器的需要,runtime库封装了所有的系统调用,将这些所谓的系统调用引入到调度器的调度点,也就是执行这样的系统调用的一个上下文将执行协程的切换。所以换句话说。Go的系统调用其实是封装好的,可以感知协程系统调用。所以从这个角度,我们也可以理解为什么Go的运行时库比较大。另外cgo的执行也是类似的过程。因为被调用的C代码极有可能通过C库执行系统调用,从而导致线程进入block,从而影响Go调度器的行为。所以我们看到cgo会因为这个原因一直执行entersyscall和exitsyscall。Rust协程绿色线程GreenThread早期的Rust支持所谓的绿色线程,其实就是栈协程的实现,和Go的协程实现很相似。0.7后删除了绿色线程。其中一个原因是,如果引入这样的机制,运行时库也必须能够像Go语言一样支持stackedcoroutines,这就是前面讨论Go题外话时提到的。Go没有原生线程的概念,只支持语言层面的协程。选择封装所有系统调用是合理的。但是,如果Rust打算这样做,那么统一Native线程和协程运行时库API的问题将很难解决。Stackless协程Stackless协程,顾名思义,是一种不使用堆栈和上下文切换来执行异步代码逻辑的机制。这里的异步代码虽然是异步的,但是执行起来却像是一个同步的过程。从这个角度来看,Rust协程与Go协程没有什么不同。例如:asyncfnroutine(){letmuta=5;睡眠(1000)。等待;一=一+1;a}几乎是相同的过程。sleep会导致sleep,时间到了,返回执行,局部变量a的内容应该还是5。Go协程是有栈的,所以这个局部变量是存放在栈上的,但是Rust是怎么实现的呢?答案是Generator生成的状态机。生成器类似于闭包。它可以捕获变量a并将其放入匿名结构中。代码中看起来像局部变量的数据a会被放入结构体中,存放在全局(线程)栈中。另外值得一提的是,Generator会生成一个状态机来保证代码的正确流转。从sleep.await返回后,将执行a=a+1行。asyncroutine()会根据内部的.await调用生成这样一个状态机,驱动代码按照既定流程执行。笼统。Stackless协程有很多好处。首先,不需要分配堆栈。因为给协程分配多少栈是个大问题。特别是在32位系统下,地址空间是有限的。每个协程都需要一个专用栈,这显然会影响可以创建的协程总数。其次,没有上下文切换,貌似性能可能更好?当然,更大的优势在于它不需要CPU系统相关的代码,因此具有更好的跨平台能力。当然,也有很多无法堆叠的问题。例如,Rust著名的PIN问题。另外,个人认为Rust的stacklesscoroutine的主要问题是没有那么直观,理解起来会有些吃力。Coroutines解决的问题直到去年年底,Rust语言才真正实现了async/await语法。在那之前,还有一些其他的临时宏替代方案。所以看现在的一些开源软件项目,真正使用await来写代码的还是比较少的,主要是poll的方式。这样的代码需要自己维护各种状态。一个经典的例子就是Sink发送的三件套:poll_ready/start_send/poll_flush。首先,您需要检查缓冲区是否有要发送的数据。如果是这样,这部分数据将被首先处理。然后检查底层是否就绪,否则无法发送。这个时候需要把当前发送的东西dump出来,就是上面说的发送缓冲区。如果你用C语言写过epoll相关的代码,你会发现和这里没有太大区别。因为这是异步编程的一般模式。其实如果能用await写代码,直接调用SinkExt的send().await方法,一切烦恼都会烟消云散。SinkExt::send内部实现了包含发送缓冲区的三件套Sink,而await则以简洁的方式优雅地呈现了这一切。这种使用.await写的代码,看起来是用同步的方式在做异步编程,比较简洁易懂。总之,我个人认为Rust异步编程的未来是await。早期手动写各种poll方法太麻烦了。语言其实是一种工具,发明出来是为了帮??助程序员,而不是为了造成更多的负担。相信这也是Rust.await最大的意义所在。在下一篇文章中,让我们看看async/await是干什么的。深圳市网华科技有限公司(Netwarps),专注于互联网安全存储领域的技术研发与应用,是一家先进的安全存储基础设施提供商。其主要产品包括去中心化文件系统(DFS)、企业联盟链平台(EAC)、区块链操作系统(BOS)。微信公众号:网华