大家好, 欢迎大家来参加本次公开课.
今天的主题是, Rust异步编程二: Tokio入门之运行时介绍.
公开课正式开始前首先我来简单的介绍一下自己.
我叫苏林, 是一名从事于互联网研发的程序员, 也是一名技术爱好者, 在互联网行业有十余年, 先后效力于电商、SaaS领域, 对底层系统级开发比较感兴趣, 也才促使我学习和探索Rust语言.
好, 开始今天正式的公开课, 今天的公开课按以下几部分来进行.
1、回顾上次公开课的内容. => 为什么要回顾上次公开课的内容, 因为异步这块确实比较复杂, 在讲公开课的时候也是采用循循渐进的方式, 回顾也是为了承上启下, 不断的深入把知识讲清楚.
2、和其它语言异步编程模型实现进行对比, 通过对比看看Rust异步编程模型实现的特点
3、async/await 底层的实现
4、谈谈我对Rust异步运行时的认识
5、Tokio介绍以及Tokio里的Executor、Reactor、Future
6、使用 Tokio 实现一个简单的服务端程序
我对这次公开课的期望 或者 说这次公开课想达到的目的.
=> 让大家对Rust异步编程生态有一个非常具体的理解并且对Rust异步编程生态中涉及到的各组件的作用有非常清晰的蓝图.
接下来, 回顾上次上开课的内容.
1、讲了为什么需要异步. => 通过单线程->多线程(线程之间切换开销之类)->异步编程(异步编程让程序不通过多线程达到类似多线程的效果)
“为什么需要异步” -> 因为我们大多数的应用需求是I/O密集型, 我们需要这样极致的性能追求, 没必要把大部分时间用到I/O等待上面, 所以通过异步能支持极致的性能.
2、对async/await语法进行了介绍 => 因为通过这种语法糖, 可以提供一种让我们像写同步代码一样来写异步代码 来实现异步程序, 这个是对编程体验的一种追求. 只介绍了 async 在编译阶段就展开成Future,
其实还有很多, async/await为什么能达到这种效果, 背后的原理是怎么的? 还需要讲明白.
3、介绍了Rust异步编程模型.
=> 我们再来回顾一下, Rust异步编程模型是所有框架实现的理论基础, 思想指导, 必须牢记于心.
开始第二部分: Rust异步编程模型的实现, 和其它语言的对比
NodeJS它也提供异步, 它也提供async/await, 那么它和Rust的区别, NodeJs依赖于V8这样一个运行时, 并且它提供的async/await是建立在Promise抽象之上, Go它内置了一个运行时(Go runtime), 它提供了goroutine这样一个协程机制. Rust相当于在语言部分只提供了async/await这样一个关键字, 还有Future只提供了语言的概念, 那么它的运行时是在语言之外, 运行时是在语言之外Tokio或者其它的, 这个都是可以替换的, 或以替换成async-std或者其它的, 这个就比较灵活, 就让Rust根据不同的场景来使用, 比如说底层操作系统像嵌入式我们就可以换成no_std的这种异步运行时, 这个是Rust相比于其它语言的一些特点, Rust它的异步相当于是第一个系统级语言内置的async/await这样一个语法, 这样一个异步语法, 语言并不内置运行时, 所以相当灵活, 并且它是零成本抽象. 相当于async/await语法糖它会是在编译器就展开成Future.
那么这个就是Rust语言提供的异步.
接下来, 我们讲一下 async/await 底层的实现.
上次公开课我们讲了async、await语法, 接下来我们介绍async、await底层的一些实现, 上节课讲async/await它实际上是解糖以后是future, 那么当你代码里await,当await去等待, 相当于在当前线程它会暂停或者说挂起, 挂起当前线程内的task, 这个是如何做到的? 它是如何去挂起的, 这个就和底层的这种叫生成器的这样一个东西, 这样一个机制来实现, 实际上就是async await对应底层的生成器就是resume和yield.
我们来看一下什么是生成器.
我们来看这段代码, 它有一个yield关键字, 我们在这里定义一个生成器, 在Rust里面就相当于一个闭包, 和闭包的区别就是能加一个yield, yield就代表一个暂停点, 当你通过一个resume, 调用一个resume时, 程序会执行到这里. 依次调用resume, 直接走完.
相当于就实现了一个迭代器的功能, 这里可能大家对这个Pin比较陌生, 我们先忽略掉它, 后面我们再讲, 如果真的要理解的话, 这里暂时把它看作是等价于把Pin等价于可变借用. 它调用这个函数.
接下来,我们看一下标准库里面是如何定义这个 Generator 的. 这个 Generator 它是一个trait, 它定义的是resume这个方法, 返回的是GeneratorState, 它有两个关联类型, yield和return, 对应Generator里面的 yield 和 return.
我们来看一下GeneratorState, 它是一个枚举, yield和完成, 是继续挂起还是最终完成, 再回过来看一下那个例子, 这样的话就起来一个协程的作用, 比如说这个函数执行到yield 1这里挂起, 然后另外一个函数,然后线程就可以去执行另外一个函数了, 达到某个条件 然后再回来执行这个resume, 然后再执行到第二个yield, 这样的话就构成了这样一个协作的条件在这个线程里面, 这是一个基础啊.
那么它和future有什么关系呢? 它是async/await异步的基础, 我们再深入一点啊, 它的本质会生成一个状态机, 看代码, 从初始状态 经过 一步一步 resume 调用, 它会慢慢的变成Done. 包括它对应的状态也会生成一些结构体, 并且为这个枚举实现Generator.
这段代码和刚才的Generator效果是一样的, 这就是Generator的一个本质, 那么Generator和future有什么关系呢, 如何把一个生成器变身为一个future, 再回到第1个例子, 我们只要限定或者规定几个状态, Pending和Ready就可以了, 相当于把yield对应Pending, Complete对应Ready. 底层做一个对应, 底层做一个包装.
实际上在Rust源码里面, 核心库future里面, 它有一个GenFuture的结构体, 它定义了一个方法from_generator, 从generator生成器变成一个future这样一个对象, 大家可以接下来去网上搜索这个源码的分析, 这里就不做细讲了, 这里只带大家看一下关键的地方, 最关键的为它实现一个future, 实现一个poll方法, 调用poll方法的时候, 相当于调用resume, Yielded对应Pending, Complete对应Ready, 这个就是generator到future的一个转换, 实际上调用poll它是一层一层的调用下来的, 最后调用最底的poll它实际上就是回归到了最底层生成器模式, Yield然后resume, Yield然后resume, …… 这样就来回达成了async await这样一个异步出让的一个协程的效果, 然后再配合着运行时, 然后根据这个Reactor这样一个事件的一个调度, 完成这个任务的这种异步的计算. 这是异步运行时整个的一个概念了.
异步块将返回一个 Future 而不是 Generator,但是 Future 的工作方式和 Generator 的内部工作方式是相似的。
Future中的每一个await就像生成器中的一个yield.
接下来, 再聊一下Rust异步运行时.
Rust异步运行时有这么几个, async-std、bastion、glommio、smol、tokio, 因为Rust标准库里面它没有自带这种运行时, 它只是提供一个异步编程的接口, 这种特性造成了Rust生态里面这种运行时比较丰富, 那么这几中运行时你该如何选择呢.
首选他们都是开源的, 这几个运行时他们各有各的特点, tokio是最早的, 最早做Rust运行时的, 所以它经历了很多的这种变化, 它是在Rust异步稳定之前就有了, 所以它有一些自己的这种特定的设计, 可能还和Rust异步这种标准有一些差异, 所以它的生态和设计不是特别通用, 它是它现在是最成熟的一个, 因为它的发展历史包括在一些公司里面应用也是比较广, 后来呢就出现了async-std, async-std它是在这个async、await语法稳定以后出现的, 因为编写异步是很困难的, 因为异步程序在运行过程中稍微碰到一点阻塞, 相当于整个进程可能会被阻塞, 然后整个程序就会卡死, 就是为了让开发者能够顺利的去编写异步程序, Rust异步工作组之前有一些人还是自己编写了async-std, Rust标准库里面都是同步的, 这个库就把标准库里面的同步提供了异步化支持, 同时它还有一个运行时.
还有一个运行时是 smol, 小的意思, 英文单词small的另外一个叫法, 它是一个比较小的运行时, 比较轻量的运行时. 这个作者不知道是什么原因反正退出了这个社区了, 虽然现在有新的维护者并且这个库己经比较稳定了, 1.0版本了, 但是现在这个库并不是很活跃, 从提交上都可以看出来. 其实最活跃的还是tokio.
所以我们重点讲tokio, tokio是生产级的.
接下来,我们开始Tokio的学习.
首先, 打开官网. 配置环境.
我们还是以hello world为例. 来进行讲解. 再去掉宏, 通过宏展开看到它的本质.
Rust语言只是提供一个零成本的异步编程抽象, 而不内置运行时.
基于Generator实现的Future, 在Future基础上提供async/await语法糖, 本质是一个状态机.
async/await在编辑器就展开成Future
为什么需要异步: => 因为我们大多数的应用需求是I/O密集型, 我们需要这样极致的性能追求, 没必要把大部分时间用到I/O等待上面, 所以通过异步能支持极致的性能.
为什么需要async/await这种语法 => 因为通过这种语法糖, 可以提供一种让我们像写同步代码一样来写异步代码 来实现异步程序, 这个是对编程体验的一种追求.
异步编程模型发展经历了三个阶段. 1、Callback 2、Promise/Future 3、async/await