关于c++:无栈协程-Rust学习笔记

77次阅读

共计 3646 个字符,预计需要花费 10 分钟才能阅读完成。

作者:谢敬伟,江湖人称“刀哥”,20 年 IT 老兵,数据通信网络专家,电信网络架构师,目前任 Netwarps 开发总监。刀哥在操作系统、网络编程、高并发、高吞吐、高可用性等畛域有多年的实践经验,并对网络及编程等方面的新技术有浓重的趣味。

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 到底做了什么。


深圳星链网科科技有限公司(Netwarps),专一于互联网安全存储畛域技术的研发与利用,是先进的平安存储基础设施提供商,次要产品有去中心化文件系统(DFS)、企业联盟链平台(EAC)、区块链操作系统(BOS)。
微信公众号:Netwarps

正文完
 0