问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Rust语言异步编程详解:从基础概念到Tokio运行时

创作时间:
2025-03-19 03:02:29
作者:
@小白创作中心

Rust语言异步编程详解:从基础概念到Tokio运行时

引用
CSDN
1.
https://m.blog.csdn.net/m0_37719524/article/details/143173548

Rust语言以其内存安全和高性能的特点,在系统编程领域备受关注。异步编程作为现代编程的重要特性之一,Rust提供了完善的异步编程支持。本文将从基础概念出发,深入讲解Rust中的异步模型、Tokio运行时的工作原理,并通过具体代码示例帮助读者理解Rust异步编程的核心机制。

通识内容

异步和多线程

异步即在不阻塞其他任务执行的情况下,并发执行多个任务,且可以在仅有几个工作线程或仅有主线程下使用。多线程则是不同的线程执行不同的任务,每个线程各司其职,或有线程间的上下文切换。简单来说:异步多数用在大量IO并发时,而多线程更适合于有大量CPU密集型任务时使用(配合线程池)。

线程用于并行工作,异步用于并行等待

Rust中的异步模型

Rust中天然支持异步编程,使用awaitasync关键字,将普通函数转变为异步函数,并通过Tokio crate的runtime环境进行轮询、切换和执行,让异步编程写起来与同步代码几乎无差别。

Rust中异步模型的概述

Rust中的异步模型,实际上是使用Future(能产出值的异步计算),通过轮询、挂起、唤醒、执行等操作来实现异步,runtime依赖Tokio。

什么是Tokio

Tokio是一个Rust异步运行时库,底层基于epoll/kqueue这样的跨平台多路IO复用以及event loop,目前正在支持io_uring。其线程会执行task,可以充分利用多核,而Task线程是Rust基于Future抽象出的一种绿色线程,因为不需要预先分配多余的栈内存,可以创建大量的Task,很适合做IO密集型应用。

Tokio runtime的任务流转

关键点:Future中的poll,Reactor,Executor,Waker,Pending,Ready,PIN,!Unpin

  1. async函数会在await时生成Future,并被主动轮询一次,如果其执行完毕,则状态被标记为Ready并在调用处即返回其值,并结束。
  2. 若没有执行结束,则被标记为Pending并会被执行器推入任务执行队列,另一方面则会挂载到events上,如epoll的红黑树等待Waker
  3. 当有Waker Ready时则会激活poll event(即所有被Waker的任务),轮询(Waker events)并依次执行,执行成功的任务会由Pending转换为Ready,进而执行完毕从任务侧去除。
  4. 而未能执行完毕的任务则会继续留存于events中,等待下一次的Waker唤醒。
  5. 直到所有任务全部执行完成。

Executor执行器会管理一批 Future (最外层的 async 函数),然后通过不停地 poll 推动它们直到完成。 最开始,执行器会先 poll 一次 Future ,后面就不会主动去 poll 了,而是等待 Future 通过调用 wake 函数来通知它可以继续,它才会继续去 poll 。这种 wake 通知然后 poll 的方式会不断重复,直到 Future 完成。

即执行器负责执行最外层async函数,或主动一次推动,或被动Waker唤醒,Poll则是具体的推动某一个Future的执行。

从代码实现角度看Rust中的异步(Tokio)

Rust中的Future定义:

trait Future {
    type Output;
    fn poll(
        // 首先值得注意的地方是,`self`的类型从`&mut self`变成了`Pin<&mut Self>`:
        self: Pin<&mut Self>,
        // 其次将`wake: fn()` 修改为 `cx: &mut Context<'_>`:
        cx: &mut Context<'_>,
    ) -> Poll<Self::Output>;
}
  1. Output: 异步函数的返回值类型
  2. Future推动者
    1. Pin<&mut Self>,固定其在内存中的内存地址(为避免自引用类型在Future被移动时失效)
    2. &mut Context<'_>,通过提供一个Waker类型的值,即可唤醒特定的任务执行
    3. PollSelf::Output 返回值类型。
pin的主要作用即将一个数据值pin在内存的固定位置,标记过unpin的数据,即便使用了Pin依然无法固定内存地址。
pin对应!unpin,在Rust中,开发者熟知的数据类型均默认实现了unpin。Pin是一个struct,而unpin是一个trait

#[tokio::main]的作用是什么

  1. Rust的main函数可以是异步函数吗?
    不可以,Rust中的异步是惰性的,被动的,而main函数是需要主动执行的。

  2. tokio是一个过程宏,tokio runtime需要在主要功能启动之前就启动,然后使用这个异步运行时来控制整个应用程序的运行。我们来看一些其真面目:

过程宏可以用编译器扩展宏,可以使用如下方式:

cargo install cargo-expand

通常我们在开发环境中使用的都是stable版本,而cargo-expand依赖nightly编译器来扩展宏,所以还需要自己安装nightl编译器。

rustup toolchain install nightly --allow-downgrade

最后我们通过命令行展开真正的main代码块:

cargo +nightly expand

展开后代码如下:

fn main() -> std::io::Result<()> {
    ...
    tokio::runtime::Builder::new_multi_thread().enable_all().build().expect("Failed building the Runtime").block_on(...)
}
#[tokio::main]任务给了我们一种能够定义异步main函数的错觉,而在实际的原理中,它只是获取了异步main函数体,并且补充了必要的样板代码
,让它可以在tokio上运行。

参考文章

[1] https://github.com/sunface/tokio-course/blob/master/src/async.md
[2] https://course.rs/advance/async/future-excuting.html
[3] https://rustmagazine.github.io/rust_magazine_2021/chapter_12/monoio.html
[4] https://tokio.rs/tokio/tutorial
[5] https://course.rs/advance/async/pin-unpin.html

如有勘误,多谢指正。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号