Skip to main content
tokio 有 什么用?

0 tokio 俯视

•1 min•193 words

学习资源

tokio 官方文档,中文翻译:
https://tokio-zh.github.io/document/

https://tokio.rust-lang.net.cn/tokio/tutorial

tokio 的选择

异步编程模型要解决的就是多异步任务高效执行与协作

tokio 如何做到?tokio 选择了什么样的解决方式?

零成本抽象

在异步编程时,总有一些高抽象操作,这些操作会代表非常多的工作

tokio 选择了零成本抽象。相比于其他语言的异步编程模型,tokio 的抽象操作与直接手写底层实现运行效率相差无几,而其他异步编程模型总是需要更多的运行时成本,来支持这些高抽象操作。

Future状态机

状态模式:状态机是编译时确定的,没有动态内存分配

通常不同状态下,不同的方法执行。我们需要运行时维护状态值与对应的分支方法。而tokio 不需要维护,静态可知内存大小。直接分配在栈上,避免了堆内存的分配与GC管理。

学过rust的面向对象编程之后,我们知道有一个状态机模式。使用trait object来将对象变成
状态对象,每个状态对应的方法具体实现都不一样,却是相同的类型。

Future 是一个实现了Future trait的结构体,Rust 的 async fn 或 async {} 块会被编译器转换为一个状态机结构体(实现 Future trait)。这个状态机是编译时确定的,没有动态内存分配(除非你显式 Box::pin)。每个 .await 点对应状态机中的一个状态,切换仅涉及字段读取和跳转,成本极低。

无栈协程与任务调度

协程:协程就是异步任务

线程可以帮助我们并行执行代码,当我们长期维护多条线程的存在,并将线程作为底层实现,新建协程任务,交给多线程运行,这样我们可以避免线程新建与销毁。并且在协程任务切换的时候,不需要切换内核线程栈,因此任务切换成本远低于线程切换。

tokio的无栈协程

协程要执行代码往往需要维护代码执行所需的局部变量,通常协程模拟线程实现,需要分配堆内存作为协程的栈空间。这就是有栈协程。
.
tokio 的无栈协程,不拥有独立的栈,而是依赖调用者的栈。Future协程任务的本体就是状态机结构体,其中保存了跨 await 点需要存活的局部变量。协程切换(即 Future 的 poll 调用)只是函数调用,不涉及栈切换,开销极小。

tokio 任务调度器:多线程工作窃取

工作窃取是一种多线程任务调度策略,用于在多个工作线程之间动态平衡负载。当一个线程完成了自己队列中的所有任务后,它会尝试从其他线程的任务队列中“偷取”任务来执行,从而避免空闲等待,提高 CPU 利用率。

  • 任务没有独立栈:Tokio 的任务只是被 Box::pin 起来的 Future(因为需要保证在堆上稳定位置,以便跨线程移动)。这个 Box 是唯一必要的堆分配,但它是显式且必要的(为了 Send 和稳定性)。

  • 调度器线程不维护复杂的上下文:切换任务仅仅是调用 Future::poll,不需要保存/恢复寄存器或切换栈。任务在 .await 挂起时,执行器会返回,然后去执行其他任务。

  • 任务队列和偷取操作高度优化:工作窃取使用无锁队列(如 crossbeam-deque),大部分时间线程只操作自己的本地队列,没有锁竞争。偷取时也是无锁或轻量级同步。

  • 任务本身没有运行时开销:任务的状态机代码与普通函数无异,调度器只是轮询它。没有类似 Go 的 runtime 调度器所需的 GMP 模型、栈管理、抢占式调度等复杂机制。

异步 I/O 与事件循环

异步IO

需要操作系统提供的非阻塞 I/O 和事件通知机制(如 epoll、kqueue、IOCP),通常由事件循环(event loop)统一管理。
当poll 内部遇到尚未就绪的 I/O 操作(如 TcpStream::read().await)时,它会向事件循环注册一个 waker(唤醒器),然后返回 Poll::Pending,将控制权交还给执行器。

事件循环

每个工作线程都有自己的事件循环(基于 epoll 等),并且与任务调度器紧密结合。
当一个任务等待 I/O 时,它被挂起,其 waker 被注册到所属线程的事件循环中。当事件就绪时,事件循环回调执行唤醒该任务,并将其放回本地队列,由调度器继续执行。

信道与原语

tokio提供了异步版本的信道与原语

tokio::sync::mpsc、Mutex、Semaphore 等。
.
tokio::sync::Mutex 在无竞争时几乎和 std::sync::Mutex 一样快,但在等待时会异步挂起而不阻塞线程,提高了并发下的吞吐量。这些原语的实现充分利用了 Rust 的所有权模型和原子操作,没有隐藏的锁或动态分配。

可靠

Rust的底层使得tokio提供了难以滥用的API

比如只给引用,控制修改与访问与获取,控制None不允许空,因为所有权有自动解锁,等等。

避免内存不安全

使用 Rust 可以消除应用程序中的整个此类错误。

灵活

tokio 提供了运行时的多种变体

从多线程、工作窃取运行时到轻量级、单线程运行时,应有尽有。这些运行时中的每一个都带有许多旋钮,允许用户根据自己的需求进行调整。

背压与消除

tokio 异步任务自然反压

在排队系统中,经常出现消耗小于生产导致缓冲无限增大的问题。问题的关键是处理负载的速度与负载的速度匹配。
.
而tokio 异步任务也是排队并发,因为 Future 是轮询驱动的每次执行都来自执行器的轮询,只有当异步操作需要执行的时候(通常是某个await 点),才会驱动异步任务加入异步任务队列。必须等待上个任务执行完毕,阻塞消除才能开始下个异步任务。
.
换句话说,轮询做完一个异步任务才会放开阻塞开始执行下个异步任务然后继续阻塞。

消除指的是不再需要的异步计算任务自动停止

异步计算任务,并非一启动就无法停止,其内部也是通过各种.await交还了控制权的。所以当我们不在需要这个计算任务的时候,么持有该计算的Future 可以被简单地丢弃(drop)。
.
然后这个计算任务之后的所有步骤自然无法得到轮询,另外Rust 的所有权模型使得 Future 在被丢弃时能够执行清理工作,所有的下游Future都会得到妥善Drop。

异步任务直接并发以及排队

除了使用await的形式驱动异步任务并发之外,还有其他形式的并发与排队。

  • tokio::spawn 可以直接将异步任务加入运行时
  • select! 将多个future直接加入运行时,哪个先完成就执行哪个分支,其余的被取消
  • join! 等待所有future全部完成(按某种顺序poll轮询这些future,而非创建新异步任务)
  • mpsc::channel 可以容纳缓冲排队多个消息
并发与排队需要注意确保并发的总量是有界的

比如缓冲区的大小,比如并发不能链式。要能控制。

tokio 不擅长的场景

不擅长:CPU密集型任务的并行执行

这种根本没必要使用tokio,并行执行CPU密集性任务直接使用轻量的多线程库即可。比如”rayon”。

Tokio 专为 IO 密集型应用程序而设计,在这些应用程序中,每个单独的任务大部分时间都花费在等待 IO 上。

如果您需要同时执行这两项操作,仍然可以“混合搭配”。

不擅长:读取大量文件

操作系统通常不提供异步文件API,所以还是得用多线程。所以 tokio 的异步在这里没有用。和普通的线程访问流程是一样的。甚至会阻塞异步执行器。

虽然看起来 Tokio 对于只需要读取大量文件的项目很有用,但与普通线程池相比,Tokio 在这里没有优势。

不应该:用于执行简单任务

比如使用异步来发送一个web request请求,这是不应该的。因为在简单任务中与阻塞 API 相比,tokio 没有真正的优势。而阻塞API会使您的项目更简单。