异步运行时

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>