Rust异步编程
包括以下内容:
当编写IO密集型应用,如服务器时,程序员常常创建大量线程并在线程之间频繁切换以应对高并发需求。传统的线程并发模型由于上下文切换成本较大,在应对这类场景时显得力不从心。其他主流的并发编程范式有以下几种:
- 事件驱动型编程。以使用回调函数为主要编程方式。其缺点在于冗长、非线性的控制流,以及难以debug或跟踪函数调用。
- 协程。例如Goroutine,类似于系统线程但开销更低。其缺点是runtime包装了过多抽象,用户无法接触底层环境。
- actor模型
- async/await
Rust选择async(异步)编程,这是一种用顺序执行的逻辑实现并发的编程方式。它实现原理较难理解,但却有诸多好处,例如与协程同样轻量化等。Rust中的协程具有一些特点,例如零额外开销,惰性求值,提供单线程分时并发等待。当面对高IO场景时,应当选择异步编程。
Rust只为async提供了必要的trait
,但没有提供实际的运行时。编写能够运行的异步代码需要依赖社区提供的第三方库,本文使用tokio
库作为范例。
Async的基本使用
在函数前添加async
关键字即可将函数转化为异步调用。
1 | async fn do_something() { |
异步函数的特点是返回值为impl Future
的对象。调用异步函数并不会执行原函数,只有在Future对象上使用关键字.await
或轮询(polling)它时,才会惰性地执行异步函数。
1 | async fn call() { |
1 | # 只有调用了.await之后才输出"Hello world!" |
当我们直接调用.await
时,程序是顺序执行的。调用.await
会使线程阻塞直到任务执行完成,这使得我们编写程序的逻辑依然是顺序的。
1 | async fn loop_3() { |
1 | # 体现顺序性 |
要使协程能够并发完成,需要依赖Executor
对Future进行轮询,其具体的原理在之后的内容中介绍。对于tokio库,我们可以使用类似Rust线程的语法完成异步并发。
1 | async fn loop_3() { |
1 | # 体现并发 |
除了在fn
前添加async
关键字外,我们还可以使用async
修饰任意表达式块。
1 | async fn loop_3() { |
async
同样存在着生命周期问题。当异步调用的函数包含引用参数时,则Future
的生命周期与引用参数一致。这意味着Future
必须在参数被drop
之前.await
。
1 | async fn foo(x: &u8) -> u8 { *x } |
要想避免这一问题,可以在该异步函数中.await
所有与引用参数相关的异步操作。
最后,由于.await只能在异步函数中调用,而Rust中的main函数原生不能声明为异步函数,故我们尚不能运行异步函数。tokio解决这一问题的方法是使用#[tokio::main]
属性为异步提供runtime,使得main可以被声明为异步。
1 |
|
1 | // 等价于 |
Async的基本原理
Future基础
Future
是异步编程的核心,它是一个可以产生值的异步计算。Future
的核心特征是它可以被轮询,轮询将推动Future
完成计算任务。
1 | fn poll(&self, wake: fn()) -> Poll<Self::Ouput> {} |
调用poll
方法进行一次轮询,当Future
完成计算时,返回Poll::Ready(T)
枚举值,否则返回Pending
表示未完成。
除了主动轮询以外,Future
还通过wake
函数主动通知执行者Executor
可以继续轮询,这避免了Executor
只能通过不断轮询判断Future
是否执行完成。
Waker与Executor
Waker
的作用是允许Future
主动通知Executor
其可以被继续轮询。
当Future
被轮询时,会接收一个带有Waker
的上下文,Future
的职责是将Waker
保存下来,并在任务已经完成时,调用该waker
对象的wake
方法通知Executor
。
1 | // Pin 表示对象在内存中不可移动,Context为包含了上下文的Waker |
Waker
则必须具有与Executor
通信的能力。Executor
是一个管理一组Future
直到其完成的对象。它的职责是在适当的时刻轮询管理的Future
,而这个时刻由Waker
决定。
我们可以假定Executor
中包含一个任务队列,一个任务包含一个Future
和一个Waker
,Executor
会不断轮询队首的Future
。对于Waker
来说,它的wake()
方法便类似于向队尾中添加其负责的Future
。
tokio的基本使用
tokio是一个提供了异步运行时,具有强大异步功能的网络编程库。
许多tokio的API都类似于Rust标准库。以tokio::spawn
为例,它接收一个Future
并提供并发能力。
1 |
|
类似于thread::spawn
,tokio::spawn
接收Future
并返回句柄,在句柄上调用.await
即类似于在线程句柄上调用join
。
除此之外,tokio还提供了一些异步的网络编程接口,如TcpStream
、TcpSocket
,其用法与标准库非常类似。当进行异步编程时,应使用tokio
提供的接口代替标准库。