异步运行时
Rust 语言本身只提供了异步编程所需的基本特性,例如 async/await
关键字,标准库中的 Future
特征,官方提供的 futures
实用库,这些特性单独使用没有任何用处,因此我们需要一个运行时来将这些特性实现的代码运行起来。
异步运行时是由 Rust 社区提供的,它们的核心是一个 reactor
和一个或多个 executor
(执行器)
简介
tokio
是 Rust 最优秀的异步运行时框架,它提供了写异步网络服务所需的几乎所有功能,不仅仅适用于大型服务器,还适用于小型嵌入式设备,它主要由以下组件构成:
- 多线程版本的异步运行时,可以运行使用
async/await
编写的代码 - 标准库中阻塞 API 的异步版本,例如
thread::sleep
会阻塞当前线程,tokio
中就提供了相应的异步实现版本 - 构建异步编程所需的生态,甚至还提供了
tracing
用于日志和分布式追踪, 提供console
用于 Debug 异步编程
劣势
- 并行运行 CPU 密集型的任务。
tokio
非常适合于 IO 密集型任务,这些 IO 任务的绝大多数时间都用于阻塞等待 IO 的结果,而不是刷刷刷的单烤 CPU。如果你的应用是 CPU 密集型(例如并行计算),建议使用rayon
,当然,对于其中的 IO 任务部分,你依然可以混用tokio
- 读取大量的文件。读取文件的瓶颈主要在于操作系统,因为 OS 没有提供异步文件读取接口,大量的并发并不会提升文件读取的并行性能,反而可能会造成不可忽视的性能损耗,因此建议使用线程(或线程池)的方式
- 发送少量 HTTP 请求。
tokio
的优势是给予你并发处理大量任务的能力,对于这种轻量级 HTTP 请求场景,tokio
除了增加你的代码复杂性,并无法带来什么额外的优势。因此,对于这种场景,你可以使用reqwest
库,它会更加简单易用。
若大家使用 tokio,那 CPU 密集的任务尤其需要用线程的方式去处理,例如使用 spawn_blocking 创建一个阻塞的线程去完成相应 CPU 密集任务。
原因是:tokio 是协作式的调度器,如果某个 CPU 密集的异步任务是通过 tokio 创建的,那理论上来说,该异步任务需要跟其它的异步任务交错执行,最终大家都得到了执行,皆大欢喜。但实际情况是,CPU 密集的任务很可能会一直霸着着 CPU,此时 tokio 的调度方式决定了该任务会一直被执行,这意味着,其它的异步任务无法得到执行的机会,最终这些任务都会因为得不到资源而饿死。 而使用 spawn_blocking 后,会创建一个单独的 OS 线程,该线程并不会被 tokio 所调度( 被 OS 所调度 ),因此它所执行的 CPU 密集任务也不会导致 tokio 调度的那些异步任务被饿死
异步初探
一个函数可以通过async fn
的方式被标记为异步函数:
use mini_redis::Result;
use mini_redis::client::Client;
use tokio::net::ToSocketAddrs;
pub async fn connect<T: ToSocketAddrs>(addr: T) -> Result<Client> {
// ...
}
async fn
异步函数并不会直接返回值,而是返回一个 Future
,顾名思义,该 Future
会在未来某个时间点被执行,然后最终获取到真实的返回值 Result<Client>
。